diff --git a/BUILD.bazel b/BUILD.bazel index 055646978..9b76c1c6d 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -68,6 +68,7 @@ cc_library( "include/nlohmann/detail/string_utils.hpp", "include/nlohmann/detail/value_t.hpp", "include/nlohmann/detail/view/builder.hpp", + "include/nlohmann/detail/view/compare.hpp", "include/nlohmann/detail/view/document_data.hpp", "include/nlohmann/detail/view/errors.hpp", "include/nlohmann/detail/view/input.hpp", @@ -80,6 +81,7 @@ cc_library( "include/nlohmann/detail/view/number.hpp", "include/nlohmann/detail/view/pointer.hpp", "include/nlohmann/detail/view/scan.hpp", + "include/nlohmann/detail/view/serializer.hpp", "include/nlohmann/detail/view/string_ref.hpp", "include/nlohmann/detail/view/value.hpp", "include/nlohmann/json.hpp", diff --git a/docs/docset/docSet.sql b/docs/docset/docSet.sql index 4764185e1..2f8685b40 100644 --- a/docs/docset/docSet.sql +++ b/docs/docset/docSet.sql @@ -154,6 +154,7 @@ INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::cbegin', 'Me INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::cend', 'Method', 'api/basic_json_view/cend/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::contains', 'Method', 'api/basic_json_view/contains/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::count', 'Method', 'api/basic_json_view/count/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::dump', 'Method', 'api/basic_json_view/dump/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::empty', 'Method', 'api/basic_json_view/empty/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::end', 'Method', 'api/basic_json_view/end/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::find', 'Method', 'api/basic_json_view/find/index.html'); @@ -176,9 +177,13 @@ INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::is_string', INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::is_structured', 'Method', 'api/basic_json_view/is_structured/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::items', 'Method', 'api/basic_json_view/items/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::materialize', 'Method', 'api/basic_json_view/materialize/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::number_format', 'Enum', 'api/basic_json_view/number_format/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::number_token', 'Method', 'api/basic_json_view/number_token/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::operator bool', 'Method', 'api/basic_json_view/operator_bool/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::operator<<', 'Operator', 'api/basic_json_view/operator_ltlt/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::operator[]', 'Operator', 'api/basic_json_view/operator[]/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::operator==', 'Operator', 'api/basic_json_view/operator_eq/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::operator!=', 'Operator', 'api/basic_json_view/operator_ne/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::size', 'Method', 'api/basic_json_view/size/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::source_offset', 'Method', 'api/basic_json_view/source_offset/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::type', 'Method', 'api/basic_json_view/type/index.html'); diff --git a/docs/mkdocs/docs/api/basic_json/dump.md b/docs/mkdocs/docs/api/basic_json/dump.md index eb13717e4..23c34cd0c 100644 --- a/docs/mkdocs/docs/api/basic_json/dump.md +++ b/docs/mkdocs/docs/api/basic_json/dump.md @@ -91,6 +91,8 @@ Binary values are serialized as an object containing two keys: - [to_string](to_string.md) returns a string representation of a JSON value - [operator<<](../operator_ltlt.md) serialize to stream +- [`basic_json_view::dump`](../basic_json_view/dump.md) the corresponding function of `basic_json_view`, serializing + directly from a flat index without building a `basic_json` value - [Serialization](../../features/serialization.md) - the serialization article ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/operator_eq.md b/docs/mkdocs/docs/api/basic_json/operator_eq.md index 58a7e74da..78002fd6c 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_eq.md +++ b/docs/mkdocs/docs/api/basic_json/operator_eq.md @@ -171,6 +171,8 @@ Linear. - [operator!=](operator_ne.md) compare for inequality - [operator<=>](operator_spaceship.md) comparison: 3-way (C++20) +- [basic_json_view::operator==](../basic_json_view/operator_eq.md) - the same comparison on a zero-copy view, without + building a `basic_json` value for it ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/operator_ne.md b/docs/mkdocs/docs/api/basic_json/operator_ne.md index ceeb31ab3..e759a6327 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_ne.md +++ b/docs/mkdocs/docs/api/basic_json/operator_ne.md @@ -95,6 +95,8 @@ Linear. - [operator==](operator_eq.md) comparison: equal - [operator<=>](operator_spaceship.md) comparison: 3-way (C++20) +- [basic_json_view::operator!=](../basic_json_view/operator_ne.md) - the same comparison on a zero-copy view, without + building a `basic_json` value for it ## Version history diff --git a/docs/mkdocs/docs/api/basic_json_view/dump.md b/docs/mkdocs/docs/api/basic_json_view/dump.md new file mode 100644 index 000000000..7cc0b6b38 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_view/dump.md @@ -0,0 +1,102 @@ +# nlohmann::basic_json_view::dump + +```cpp +string_t dump(const int indent = -1, + const char indent_char = ' ', + const bool ensure_ascii = false, + const number_format numbers = number_format::shortest) const; +``` + +Serializes this value (and its subtree) directly from the flat index, without ever building a `BasicJsonType` value +first. With the default `#!cpp numbers == number_format::shortest`, the result is the same string +[`BasicJsonType::dump`](../basic_json/dump.md) would produce for the value +[`BasicJsonType::parse()`](../basic_json/parse.md) builds from the same source text, called with the same `indent`, +`indent_char`, and `ensure_ascii` -- except that members of an object appear in document order rather than sorted by +key, and *every* occurrence of a repeated key is written rather than only the last one (see +[Notes on duplicate keys](operator[].md#notes)). For a `json_view` (whose `BasicJsonType` is not ordered), this means +`dump()` can print an object's members in a different order than [`materialize()`](materialize.md)`.dump()` of the +same subtree. + +## Parameters + +`indent` (in) +: If `indent` is nonnegative, array elements and object members are pretty-printed with that indent level. An + indent level of `0` only inserts newlines. `-1` (the default) selects the most compact representation. + +`indent_char` (in) +: The character used for indentation if `indent` is greater than `0`. The default is ` ` (space). + +`ensure_ascii` (in) +: If `ensure_ascii` is `#!cpp true`, all non-ASCII characters in the output are escaped with `\uXXXX` sequences, and + the result consists of ASCII characters only. + +`numbers` (in) +: how to write numbers, see [`number_format`](number_format.md): `shortest` (the default) writes them the way + [`BasicJsonType::dump`](../basic_json/dump.md) would; `source` copies every number exactly as it appears in the + source text. + +## Return value + +string containing the serialization of this value, or `#!cpp ""` if the view is +[discarded](is_discarded.md). + +## Exception safety + +Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to. + +## Exceptions + +May throw `#!cpp std::bad_alloc` if allocating the output string fails. Unlike +[`BasicJsonType::dump`](../basic_json/dump.md), there is no `error_handler` parameter and no +[`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316): the view only ever holds text the parser +already validated as UTF-8, so there is nothing to replace or ignore. + +## Complexity + +Linear in the size of the output text. + +## Notes + +The walk over the subtree is iterative, so the nesting depth it can write is limited by available memory only, not by +the call stack -- as for [`materialize()`](materialize.md). + +Strings are escaped by the same rules as [`BasicJsonType::dump`](../basic_json/dump.md). With +`#!cpp numbers == number_format::shortest`, floats are written with the library's shortest round-trip conversion, +exactly as [`BasicJsonType::dump`](../basic_json/dump.md) would (e.g. `#!cpp 1.5`, `#!cpp 100.0`, `#!cpp 1e+100`), and +integers are copied from the source text -- already canonical in JSON, so this matches their shortest form too -- +except that `#!cpp -0` is written as `#!cpp 0`, the way [`BasicJsonType::parse()`](../basic_json/parse.md) reads it. +`#!cpp number_format::source` copies every number exactly as written in the source text instead, with no exception +for `#!cpp -0` -- `#!cpp 1.50`, `#!cpp 1E2`, `#!cpp -0.0`, `#!cpp -0`, or all digits of an integer literal with more +digits than any number type holds (such a literal is itself classified as a float, see +[What is different](../../features/json_view.md#what-is-different)) -- something `BasicJsonType` cannot do, since +parsing already reduces every number to its parsed value. + +## Examples + +??? example + + The example below forwards a single record out of a larger batch, and re-serializes a configuration file, both + without ever building a `BasicJsonType` value for the surrounding array or for the parts of it that were not + needed. It also shows that [`materialize()`](materialize.md)`.dump()` of the configuration sorts its keys, where + `dump()` on the view keeps the order they appear in the source text. + + ```cpp + --8<-- "examples/basic_json_view__dump.cpp" + ``` + + Output: + + ```json + --8<-- "examples/basic_json_view__dump.output" + ``` + +## See also + +- [`number_format`](number_format.md) - how `dump()` writes numbers +- [operator<<](operator_ltlt.md) - serialize this value to a stream +- [materialize](materialize.md) - build a `BasicJsonType` value, e.g. to use `BasicJsonType::dump`'s `error_handler` +- [`BasicJsonType::dump`](../basic_json/dump.md) - the corresponding function of `basic_json` + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json_view/index.md b/docs/mkdocs/docs/api/basic_json_view/index.md index 50bcaa24b..56760db2f 100644 --- a/docs/mkdocs/docs/api/basic_json_view/index.md +++ b/docs/mkdocs/docs/api/basic_json_view/index.md @@ -20,11 +20,12 @@ Moving the document itself does not invalidate its views: the index is heap-allo `basic_json_document` object. `basic_json_view` provides the read-only part of the `BasicJsonType` interface: the type-inspection functions, element -access, lookup, iteration, and conversion -- [`get()`](get.md), [`get_string()`](get_string.md), +access, lookup, iteration, conversion, and comparison -- [`get()`](get.md), [`get_string()`](get_string.md), [`number_token()`](number_token.md), and [`materialize()`](materialize.md) to build the `BasicJsonType` value of a subtree on demand. [`operator[]`](operator%5B%5D.md), [`at`](at.md), [`contains`](contains.md), and -[`value`](value.md) also accept a [`json_pointer`](../json_pointer/index.md). It does not (yet) provide `dump()` or -comparison. +[`value`](value.md) also accept a [`json_pointer`](../json_pointer/index.md). [`operator==`](operator_eq.md) and +[`operator!=`](operator_ne.md) compare two views, or a view and a `BasicJsonType` value, without ever building a +`BasicJsonType` value for a view; no ordering comparison (`#!cpp operator<`) is provided. ## Template parameters @@ -47,6 +48,7 @@ comparison. - **iterator**, **const_iterator** - a forward iterator over the elements of an array or the member values of an object, in document order; both names refer to the same type, since a view is always read-only - **item** - a (key, value) pair produced by [`items()`](items.md) +- [**number_format**](number_format.md) - how [`dump()`](dump.md) writes numbers ## Member functions @@ -106,6 +108,16 @@ comparison. - [**number_token**](number_token.md) - get a number's token text without a copy - [**materialize**](materialize.md) - build the `BasicJsonType` value of this subtree +### Comparison + +- [**operator==**](operator_eq.md) - comparison: equal +- [**operator!=**](operator_ne.md) - comparison: not equal + +### Serialization + +- [**dump**](dump.md) - serialize to a JSON-formatted string +- [**operator<<**](operator_ltlt.md) - serialize to stream + ### Source access - [**source_offset**](source_offset.md) - byte offset of this value in the document's source text diff --git a/docs/mkdocs/docs/api/basic_json_view/number_format.md b/docs/mkdocs/docs/api/basic_json_view/number_format.md new file mode 100644 index 000000000..6b8c6805d --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_view/number_format.md @@ -0,0 +1,51 @@ +# nlohmann::basic_json_view::number_format + +```cpp +enum class number_format { + shortest, + source +}; +``` + +This enumeration is used in [`dump`](dump.md) to choose how numbers are written. Two values are differentiated: + +shortest +: integers are copied from the source text -- already canonical in JSON -- except that `#!cpp -0` becomes + `#!cpp 0`, the way [`BasicJsonType::parse()`](../basic_json/parse.md) reads it; floats are written with the + library's shortest round-trip conversion, exactly as [`BasicJsonType::dump()`](../basic_json/dump.md) would (e.g. + `#!cpp 1.5`, `#!cpp 100.0`, `#!cpp 1e+100`) + +source +: every number is copied exactly as it appears in the source text -- `#!cpp 1.50`, `#!cpp 1E2`, `#!cpp -0`, all + digits of an integer literal with more digits than any number type holds -- something `BasicJsonType` cannot do, + since parsing already reduces every number to its parsed value + +## Examples + +??? example + + The example below writes back a price list received from a supplier: with `number_format::shortest` (the + default), a trailing zero and scientific notation are normalized away and a long account number that overflows + every number type is rounded, the same way `#!cpp materialize().dump()` (or `basic_json::dump()`) would; + `number_format::source` keeps every number exactly as it was written in the source text instead. + + ```cpp + --8<-- "examples/basic_json_view__number_format.cpp" + ``` + + Output: + + ```json + --8<-- "examples/basic_json_view__number_format.output" + ``` + +## See also + +- [dump](dump.md) - serialize to a JSON-formatted string +- [number_token](number_token.md) - get a single number's token text without dumping the whole value +- [`BasicJsonType::error_handler_t`](../basic_json/error_handler_t.md) - the analogous enumeration for + `BasicJsonType::dump`'s decoding-error behavior + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json_view/operator_eq.md b/docs/mkdocs/docs/api/basic_json_view/operator_eq.md new file mode 100644 index 000000000..6f96f4692 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_view/operator_eq.md @@ -0,0 +1,107 @@ +# nlohmann::basic_json_view::operator== + +```cpp +// (1) +bool operator==(const basic_json_view& lhs, const basic_json_view& rhs); + +// (2) +bool operator==(const basic_json_view& lhs, const BasicJsonType& rhs); +bool operator==(const BasicJsonType& lhs, const basic_json_view& rhs); +``` + +1. Compares two views for equality: whether the values [`BasicJsonType::parse()`](../basic_json/parse.md) would + produce for `lhs` and `rhs` are equal, according to `BasicJsonType`'s [`operator==`](../basic_json/operator_eq.md). +2. Compares a view and a `BasicJsonType` value for equality, in either order: whether the value `parse()` would + produce for the view and the other operand are equal, according to `BasicJsonType`'s + [`operator==`](../basic_json/operator_eq.md). + +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. + +## Parameters + +`lhs` (in) +: first value to consider + +`rhs` (in) +: second value to consider + +## Return value + +whether the values `lhs` and `rhs` are equal + +## Exception safety + +Strong exception safety: if an exception is thrown, there are no changes to either operand, or to the document(s) a +view refers to. + +## Exceptions + +May throw `#!cpp std::bad_alloc`. Unlike the other comparison and most other `basic_json_view` functions, +`operator==` is not `#!cpp noexcept`: resolving an object's members needs a temporary array to sort them by key (see +[Complexity](#complexity) below), and that allocation can fail. + +## Complexity + +Linear in the size of the compared values: every number, string, array element, and object member is visited at most +once, and the walk is iterative, so the nesting depth it can compare is limited by available memory only, not by the +call stack (as for [`materialize()`](materialize.md)). Resolving an object's members takes an additional O(n log n) +in the number of members at that level, since they are sorted by key to detect and resolve duplicates before being +compared. Two arrays of different [`size()`](size.md) are rejected without visiting either one's elements. + +## Notes + +Only a single number, boolean, or `#!cpp null` value is ever materialized into a `BasicJsonType`, to reuse its +`operator==` -- for numbers, so that values written differently in the source text but equal in value (e.g. an +integer and a floating-point literal) still compare equal, following the same rules `BasicJsonType` does for special +values such as `#!cpp NaN`. Constructing one of these scalars never allocates. Strings are compared directly, without +allocating, either from the source text on both sides or, for overload 2, against `BasicJsonType`'s own string. +Arrays and objects are never materialized at all; only their elements or members are visited, one pair at a time. + +!!! info "How objects are compared" + + For a [`json_view`](../json_view.md) (`BasicJsonType::object_t` is `#!cpp std::map`), members are compared by + key, regardless of the order they appear in the source text. For an + [`ordered_json_view`](../ordered_json_view.md) (`object_t` is `ordered_map`), they are compared in the order + they occur, so the very same two objects with their members reordered can compare equal as `json_view`s but not + as `ordered_json_view`s. This is exactly how [`json`](../json.md) and [`ordered_json`](../ordered_json.md) + compare, see ["Comparing different `basic_json` specializations"](../basic_json/operator_eq.md#notes). + +!!! info "Discarded views" + + A [discarded](is_discarded.md) view compares the same way a discarded `BasicJsonType` value does, which is + governed by + [`JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON`](../macros/json_use_legacy_discarded_value_comparison.md): by + default, a discarded view is never equal to anything, not even another discarded view. + +No ordering comparison (`#!cpp operator<`) is provided for `basic_json_view`; [`materialize()`](materialize.md) is +the way to get a `BasicJsonType` value that supports it. + +## Examples + +??? example + + The example below checks whether a newly received configuration differs from the previous one, and whether a + received document matches what a test expects -- directly on views, without ever materializing a `BasicJsonType` + value for either side. + + ```cpp + --8<-- "examples/basic_json_view__operator_eq.cpp" + ``` + + Output: + + ```json + --8<-- "examples/basic_json_view__operator_eq.output" + ``` + +## See also + +- [operator!=](operator_ne.md) - compare for inequality +- [materialize](materialize.md) - build a `BasicJsonType` value, e.g. to keep comparing after the document is gone +- [`BasicJsonType::operator==`](../basic_json/operator_eq.md) - the corresponding function of `basic_json` + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json_view/operator_ltlt.md b/docs/mkdocs/docs/api/basic_json_view/operator_ltlt.md new file mode 100644 index 000000000..c2735b360 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_view/operator_ltlt.md @@ -0,0 +1,74 @@ +# nlohmann::basic_json_view::operator<< + +```cpp +std::ostream& operator<<(std::ostream& o, const basic_json_view& v); +``` + +Not available when [`JSON_NO_IO`](../macros/json_no_io.md) is defined. + +Serializes the given view `v` to the output stream `o`, using [`dump`](dump.md) -- exactly as +`#!cpp operator<<(std::ostream&, const basic_json&)` does for a `basic_json` value. + +- The indentation of the output can be controlled with the member variable `width` of the output stream `o`. For + instance, using the manipulator `std::setw(4)` on `o` sets the indentation level to `4`, and the serialization + result is the same as calling `#!cpp v.dump(4)`. A `width` of `0` or less (the default) selects the most compact + representation, as `#!cpp v.dump(-1)` does. +- The indentation character can be controlled with the member variable `fill` of the output stream `o`. For instance, + the manipulator `std::setfill('\t')` sets indentation to use a tab character rather than the default space + character. +- As for `basic_json`, `o`'s `width` is reset to `0` after this call, whether or not it was greater than `0` before. + +Numbers are always written as `#!cpp v.dump()` writes them by default, i.e. as with +[`number_format::shortest`](number_format.md); there is no way to select `#!cpp number_format::source` through the +stream. + +## Parameters + +`o` (in, out) +: stream to write to + +`v` (in) +: view to serialize + +## Return value + +the stream `o` + +## Exceptions + +May throw `#!cpp std::bad_alloc`, propagated from [`dump`](dump.md#exceptions). Unlike +`#!cpp operator<<(std::ostream&, const basic_json&)`, there is no UTF-8 decoding step that could throw +[`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316), and no `error_handler` to choose between -- +see the [Exceptions](dump.md#exceptions) of `dump`. + +## Complexity + +Linear, as [`dump`](dump.md#complexity). + +## Examples + +??? example + + The example below writes one record out of a larger batch straight to a log stream -- compact for a one-line + entry, and pretty-printed with `std::setw`/`std::setfill` for a readable dump -- without ever building a + `BasicJsonType` value for the record, or for the rest of the batch. + + ```cpp + --8<-- "examples/basic_json_view__operator_ltlt.cpp" + ``` + + Output: + + ```json + --8<-- "examples/basic_json_view__operator_ltlt.output" + ``` + +## See also + +- [dump](dump.md) - serialize to a JSON-formatted string +- [`operator<<(std::ostream&)`](../operator_ltlt.md) - the corresponding operator for `basic_json` +- [`JSON_NO_IO`](../macros/json_no_io.md) - switch off functions relying on certain C++ I/O headers + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json_view/operator_ne.md b/docs/mkdocs/docs/api/basic_json_view/operator_ne.md new file mode 100644 index 000000000..2e85f8abe --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_view/operator_ne.md @@ -0,0 +1,82 @@ +# nlohmann::basic_json_view::operator!= + +```cpp +// (1) +bool operator!=(const basic_json_view& lhs, const basic_json_view& rhs); + +// (2) +bool operator!=(const basic_json_view& lhs, const BasicJsonType& rhs); +bool operator!=(const BasicJsonType& lhs, const basic_json_view& rhs); +``` + +1. Compares two views for inequality. Returns `#!cpp !(lhs == rhs)`, see [operator==](operator_eq.md). +2. Compares a view and a `BasicJsonType` value for inequality, in either order. Returns `#!cpp !(lhs == rhs)` (or, + for the reversed order, `#!cpp !(rhs == lhs)`), see [operator==](operator_eq.md). + +Since `operator!=` is defined as the negation of [`operator==`](operator_eq.md), it follows the same rules for +special cases: for instance, since a [discarded](is_discarded.md) view is never equal to anything by default (see +[operator=='s Notes](operator_eq.md#notes)), it is never *unequal* to anything either -- `#!cpp discarded != discarded` +is also `#!cpp false`, exactly as for a discarded `BasicJsonType` value. + +## Parameters + +`lhs` (in) +: first value to consider + +`rhs` (in) +: second value to consider + +## Return value + +whether the values `lhs` and `rhs` are not equal + +## Exception safety + +Strong exception safety: if an exception is thrown, there are no changes to either operand, or to the document(s) a +view refers to. + +## Exceptions + +May throw `#!cpp std::bad_alloc`, propagated from [`operator==`](operator_eq.md#exceptions). Unlike most other +`basic_json_view` functions, `operator!=` is not `#!cpp noexcept`. + +## Complexity + +Linear, as [`operator==`](operator_eq.md#complexity). + +## Notes + +See the [Notes](operator_eq.md#notes) of `operator==` -- in particular for how an object's members are compared +(order matters for [`ordered_json_view`](../ordered_json_view.md) but not for [`json_view`](../json_view.md)) and +for how discarded views compare. + +No ordering comparison (`#!cpp operator<`) is provided for `basic_json_view`; [`materialize()`](materialize.md) is +the way to get a `BasicJsonType` value that supports it. + +## Examples + +??? example + + The example below asserts, as a test would, that a received document differs from an unwanted value, and shows + that -- as for [`json`](../json.md)/[`ordered_json`](../ordered_json.md) -- reordering an object's members is + detected as a difference for an `ordered_json_view` but not for a `json_view`. + + ```cpp + --8<-- "examples/basic_json_view__operator_ne.cpp" + ``` + + Output: + + ```json + --8<-- "examples/basic_json_view__operator_ne.output" + ``` + +## See also + +- [operator==](operator_eq.md) - compare for equality +- [materialize](materialize.md) - build a `BasicJsonType` value, e.g. to keep comparing after the document is gone +- [`BasicJsonType::operator!=`](../basic_json/operator_ne.md) - the corresponding function of `basic_json` + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/operator_ltlt.md b/docs/mkdocs/docs/api/operator_ltlt.md index da53d7ee1..6a821b7fb 100644 --- a/docs/mkdocs/docs/api/operator_ltlt.md +++ b/docs/mkdocs/docs/api/operator_ltlt.md @@ -86,6 +86,8 @@ Linear. ## See also - [dump](basic_json/dump.md) - serialize to a JSON-formatted string +- [`basic_json_view::operator<<`](basic_json_view/operator_ltlt.md) - the corresponding operator for + `basic_json_view` - [Serialization](../features/serialization.md) - the serialization article ## Version history diff --git a/docs/mkdocs/docs/examples/basic_json_view__dump.cpp b/docs/mkdocs/docs/examples/basic_json_view__dump.cpp new file mode 100644 index 000000000..c9bd8198f --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_view__dump.cpp @@ -0,0 +1,25 @@ +#include +#include + +using json_document = nlohmann::json_document; +using json_view = nlohmann::json_view; + +int main() +{ + // a large batch of sensor readings -- forward just the one that changed, + // without ever building a basic_json value for the batch or for the + // readings that are not needed + const json_document batch = json_document::parse(R"( + [{"id": 1, "temp": 21.5}, {"id": 2, "temp": 87.3}, {"id": 3, "temp": 21.7}] + )"); + const json_view readings = batch.root(); + std::cout << readings[1].dump() << '\n'; + + // a configuration file -- dump() on the view keeps the member order of + // the source text; a json value's object_t is std::map, so + // materialize().dump() of the very same view sorts the keys instead + const json_document config = json_document::parse( + R"({"name": "cache", "host": "db1", "port": 6379, "timeout": 30})"); + std::cout << config.root().dump(2) << "\n\n"; + std::cout << config.root().materialize().dump(2) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/basic_json_view__dump.output b/docs/mkdocs/docs/examples/basic_json_view__dump.output new file mode 100644 index 000000000..e6e03522c --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_view__dump.output @@ -0,0 +1,14 @@ +{"id":2,"temp":87.3} +{ + "name": "cache", + "host": "db1", + "port": 6379, + "timeout": 30 +} + +{ + "host": "db1", + "name": "cache", + "port": 6379, + "timeout": 30 +} diff --git a/docs/mkdocs/docs/examples/basic_json_view__number_format.cpp b/docs/mkdocs/docs/examples/basic_json_view__number_format.cpp new file mode 100644 index 000000000..f67ea4e84 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_view__number_format.cpp @@ -0,0 +1,28 @@ +#include +#include + +using json_document = nlohmann::json_document; +using json_view = nlohmann::json_view; + +int main() +{ + // a price list received from a supplier feed -- prices and account + // numbers must be forwarded exactly, e.g. into an invoice + const json_document doc = json_document::parse(R"( + [{"sku": "A1", "price": 19.90, "account_id": 12345678901234567890123456}, + {"sku": "A2", "price": 1E2, "account_id": 98765432109876543210987654}] + )"); + const json_view list = doc.root(); + + // number_format::shortest (the default) writes numbers the way + // basic_json::dump() would: "19.90" becomes "19.9", "1E2" becomes + // "100.0", and each account number -- far beyond any 64-bit integer -- + // is rounded to the nearest double, exactly as materialize().dump() + // (or a plain nlohmann::json) would round it + std::cout << list.dump() << '\n'; + + // number_format::source copies every number exactly as it was written + // in the source text instead -- something basic_json cannot do at all, + // since parsing already reduces every number to its parsed value + std::cout << list.dump(-1, ' ', false, json_view::number_format::source) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/basic_json_view__number_format.output b/docs/mkdocs/docs/examples/basic_json_view__number_format.output new file mode 100644 index 000000000..b4ab72d54 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_view__number_format.output @@ -0,0 +1,2 @@ +[{"sku":"A1","price":19.9,"account_id":1.2345678901234568e+25},{"sku":"A2","price":100.0,"account_id":9.876543210987655e+25}] +[{"sku":"A1","price":19.90,"account_id":12345678901234567890123456},{"sku":"A2","price":1E2,"account_id":98765432109876543210987654}] diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_eq.cpp b/docs/mkdocs/docs/examples/basic_json_view__operator_eq.cpp new file mode 100644 index 000000000..846d40fc0 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_view__operator_eq.cpp @@ -0,0 +1,30 @@ +#include +#include + +using json_document = nlohmann::json_document; +using json = nlohmann::json; + +int main() +{ + // two snapshots of a polled configuration endpoint -- compare them + // directly as views, without ever building a nlohmann::json value for + // either one + const json_document previous = json_document::parse( + R"({"name": "cache", "port": 6379, "timeout": 30})"); + const json_document current = json_document::parse( + R"({"port": 6379.0, "timeout": 30, "name": "cache"})"); + + // same members, reordered, and 6379 written as a float -- operator== + // treats them the same way BasicJsonType::operator== would + std::cout << std::boolalpha << (previous.root() == current.root()) << '\n'; + + // an actually changed value is detected the same way + const json_document changed = json_document::parse( + R"({"name": "cache", "port": 6380, "timeout": 30})"); + std::cout << (previous.root() == changed.root()) << '\n'; + + // comparing a view directly against an expected json value -- handy in a + // test, without materializing the received document at all + const json expected = {{"name", "cache"}, {"port", 6379}, {"timeout", 30}}; + std::cout << (previous.root() == expected) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_eq.output b/docs/mkdocs/docs/examples/basic_json_view__operator_eq.output new file mode 100644 index 000000000..87f8d93a0 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_view__operator_eq.output @@ -0,0 +1,3 @@ +true +false +true diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.cpp b/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.cpp new file mode 100644 index 000000000..97100d536 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.cpp @@ -0,0 +1,26 @@ +#include +#include +#include + +using json_document = nlohmann::json_document; +using json_view = nlohmann::json_view; + +int main() +{ + // one order out of a large incoming batch -- write it straight to a log + // stream without ever building a basic_json value for it, or for the + // rest of the batch + const json_document doc = json_document::parse(R"( + [{"id": 1, "item": "cable"}, {"id": 2, "item": "adapter"}] + )"); + const json_view orders = doc.root(); + + // compact, for a one-line log entry + std::cout << orders[1] << '\n'; + + // std::setw sets the indentation level, exactly as for basic_json + std::cout << std::setw(2) << orders[1] << "\n\n"; + + // std::setfill changes the indentation character + std::cout << std::setw(1) << std::setfill('\t') << orders[1] << '\n'; +} diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.output b/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.output new file mode 100644 index 000000000..d791fc312 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.output @@ -0,0 +1,10 @@ +{"id":2,"item":"adapter"} +{ + "id": 2, + "item": "adapter" +} + +{ + "id": 2, + "item": "adapter" +} diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_ne.cpp b/docs/mkdocs/docs/examples/basic_json_view__operator_ne.cpp new file mode 100644 index 000000000..faa1d61cf --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_view__operator_ne.cpp @@ -0,0 +1,28 @@ +#include +#include + +using json_document = nlohmann::json_document; +using ordered_json_document = nlohmann::ordered_json_document; +using json = nlohmann::json; + +int main() +{ + // assert, as a test would, that a received document differs from an + // unwanted shape -- without ever materializing it into a json value just + // to compare + const json_document received = json_document::parse( + R"({"status": "ok", "code": 200})"); + const json unwanted = {{"status", "error"}, {"code", 500}}; + std::cout << std::boolalpha << (received.root() != unwanted) << '\n'; + + // json (std::map) compares object members regardless of order ... + const json_document a = json_document::parse(R"({"a": 1, "b": 2})"); + const json_document b = json_document::parse(R"({"b": 2, "a": 1})"); + std::cout << (a.root() != b.root()) << '\n'; + + // ... but ordered_json (ordered_map) compares them in the order they + // appear, so the very same reordering is detected as a difference + const ordered_json_document oa = ordered_json_document::parse(R"({"a": 1, "b": 2})"); + const ordered_json_document ob = ordered_json_document::parse(R"({"b": 2, "a": 1})"); + std::cout << (oa.root() != ob.root()) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_ne.output b/docs/mkdocs/docs/examples/basic_json_view__operator_ne.output new file mode 100644 index 000000000..87f8d93a0 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_view__operator_ne.output @@ -0,0 +1,3 @@ +true +false +true diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 7dbea9241..8030fe115 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -139,8 +139,12 @@ whenever any of the other conditions above was not met. 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 `BasicJsonType` was built. -- **`dump()` and comparison are not (yet) provided** by `basic_json_view`. For now, - [`materialize()`](../api/basic_json_view/materialize.md) is the way to get a value you can do those things with. +- **Ordering comparisons are not provided** by `basic_json_view` -- there is no `#!cpp operator<`. + [`operator==`](../api/basic_json_view/operator_eq.md) and [`operator!=`](../api/basic_json_view/operator_ne.md) are + provided, though: two views, or a view and a `BasicJsonType` value, compare equal exactly when + [`materialize()`](../api/basic_json_view/materialize.md) or [`parse()`](../api/basic_json/parse.md) would produce + 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. ## Getting values out without copying @@ -166,6 +170,23 @@ Two conversions never copy at all: Both results are only valid as long as the view -- and, for a string with no escapes, the borrowed source text -- is. +## Writing a view back + +[`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`. + +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 +written in the source text -- a price like `#!cpp 19.90`, a long order or account ID with more digits than any number +type holds, or a high-precision coordinate -- something `basic_json` cannot do at all, since parsing already reduces +a number to its parsed `#!cpp double`/`#!cpp int64_t` value. + +[`operator<<`](../api/basic_json_view/operator_ltlt.md) writes a view to a stream the way `basic_json`'s does, using +the stream's `width`/`fill` for indentation. + ## Choosing between `json`, `ordered_json`, the SAX interface, and `json_view` | | [`json`](../api/json.md) / [`ordered_json`](../api/ordered_json.md) | [SAX interface](parsing/sax_interface.md) | [`json_document`](../api/json_document.md) / [`json_view`](../api/json_view.md) | diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 32da88c70..d5adb9476 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -257,6 +257,7 @@ nav: - 'cend': api/basic_json_view/cend.md - 'contains': api/basic_json_view/contains.md - 'count': api/basic_json_view/count.md + - 'dump': api/basic_json_view/dump.md - 'empty': api/basic_json_view/empty.md - 'end': api/basic_json_view/end.md - 'find': api/basic_json_view/find.md @@ -279,9 +280,13 @@ nav: - 'is_structured': api/basic_json_view/is_structured.md - 'items': api/basic_json_view/items.md - 'materialize': api/basic_json_view/materialize.md + - 'number_format': api/basic_json_view/number_format.md - 'number_token': api/basic_json_view/number_token.md - 'operator bool': api/basic_json_view/operator_bool.md + - 'operator<<': api/basic_json_view/operator_ltlt.md - 'operator[]': api/basic_json_view/operator[].md + - 'operator==': api/basic_json_view/operator_eq.md + - 'operator!=': api/basic_json_view/operator_ne.md - 'size': api/basic_json_view/size.md - 'source_offset': api/basic_json_view/source_offset.md - 'type': api/basic_json_view/type.md diff --git a/include/nlohmann/detail/view/compare.hpp b/include/nlohmann/detail/view/compare.hpp new file mode 100644 index 000000000..9a50590a3 --- /dev/null +++ b/include/nlohmann/detail/view/compare.hpp @@ -0,0 +1,317 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // sort, stable_sort +#include // size_t +#include // string +#include // move, pair +#include // vector + +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +// Equality of views, and of views with basic_json values, with the semantics +// of basic_json's operator== applied to the values parse() would produce: +// numbers compare by value across their types, an object is compared by its +// members with duplicate keys resolved as parse() resolves them (the last +// value, at the position of the first occurrence), and in document order if +// the object type keeps an order (ordered_json), by key otherwise. + +/// one side of a comparison: a view +template +class view_side +{ + public: + using string_view_t = typename View::string_view_t; + + explicit view_side(const View& v) noexcept + : m_view(v) + {} + + value_t type() const noexcept + { + return m_view.type(); + } + + std::size_t size() const noexcept + { + return m_view.size(); + } + + string_view_t string() const + { + return m_view.get_string(); + } + + /// a number, boolean, or null as a basic_json value (no allocation) + BasicJsonType scalar() const + { + switch (m_view.type()) + { + case value_t::number_integer: + return BasicJsonType(m_view.template get()); + case value_t::number_unsigned: + return BasicJsonType(m_view.template get()); + case value_t::number_float: + return BasicJsonType(m_view.template get()); + case value_t::boolean: + return BasicJsonType(m_view.template get()); + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + case value_t::discarded: + default: + return BasicJsonType(nullptr); + } + } + + void elements(std::vector& out) const + { + out.reserve(m_view.size()); + for (const View e : m_view) + { + out.emplace_back(e); + } + } + + /// the members as parse() keeps them: one per key, the last value at the + /// position of the first occurrence; in that order, or sorted by key + void members(std::vector>& out, bool ordered) const + { + struct member + { + string_view_t key; + View value; + std::size_t position; + }; + std::vector all; + all.reserve(m_view.size()); + std::size_t position = 0; + for (auto it = m_view.begin(); it != m_view.end(); ++it) + { + all.push_back(member{it.key(), it.value(), position++}); + } + std::stable_sort(all.begin(), all.end(), [](const member & a, const member & b) + { + return a.key < b.key; + }); + std::vector unique; + unique.reserve(all.size()); + for (std::size_t i = 0; i < all.size();) + { + std::size_t last = i; + while (last + 1 < all.size() && all[last + 1].key == all[i].key) + { + ++last; + } + unique.push_back(member{all[i].key, all[last].value, all[i].position}); + i = last + 1; + } + if (ordered) + { + std::sort(unique.begin(), unique.end(), [](const member & a, const member & b) + { + return a.position < b.position; + }); + } + out.reserve(unique.size()); + for (const member& m : unique) + { + out.emplace_back(m.key, view_side(m.value)); + } + } + + private: + View m_view; +}; + +/// the other side of a comparison: a basic_json value +template +class json_side +{ + public: + using string_view_t = StringView; + + explicit json_side(const BasicJsonType& j) noexcept + : m_json(&j) + {} + + value_t type() const noexcept + { + return m_json->type(); + } + + std::size_t size() const noexcept + { + return m_json->size(); + } + + string_view_t string() const + { + const auto& s = m_json->template get_ref(); + return string_view_t(s.data(), s.size()); + } + + BasicJsonType scalar() const + { + return *m_json; + } + + void elements(std::vector& out) const + { + out.reserve(m_json->size()); + for (const auto& e : *m_json) + { + out.emplace_back(e); + } + } + + void members(std::vector>& out, bool ordered) const + { + out.reserve(m_json->size()); + for (auto it = m_json->cbegin(); it != m_json->cend(); ++it) + { + out.emplace_back(string_view_t(it.key().data(), it.key().size()), json_side(it.value())); + } + if (!ordered) + { + std::sort(out.begin(), out.end(), [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + } + } + + private: + const BasicJsonType* m_json; +}; + +/// whether two sides are equal; iterative, so that the nesting depth is +/// limited by memory only +template +bool equal(const A& a0, const B& b0) +{ + using string_view_t = typename A::string_view_t; + const bool ordered = is_ordered_map::value; + + struct frame + { + std::vector elements_a{}; + std::vector elements_b{}; + std::vector> members_a{}; + std::vector> members_b{}; + bool object = false; + std::size_t next = 0; + }; + std::vector stack; + A a = a0; + B b = b0; + for (;;) + { + const value_t ta = a.type(); + const value_t tb = b.type(); + const bool numbers = (ta == value_t::number_integer || ta == value_t::number_unsigned || ta == value_t::number_float) + && (tb == value_t::number_integer || tb == value_t::number_unsigned || tb == value_t::number_float); + if (ta == value_t::discarded || tb == value_t::discarded) + { + // basic_json decides (JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON) + if (ta != tb || !(BasicJsonType(value_t::discarded) == BasicJsonType(value_t::discarded))) + { + return false; + } + } + else + { + if (!numbers && ta != tb) + { + return false; + } + if (ta == value_t::string) + { + if (!(a.string() == b.string())) + { + return false; + } + } + else if (ta == value_t::array || ta == value_t::object) + { + if (a.size() != b.size() && ta == value_t::array) + { + return false; + } + frame f; + f.object = ta == value_t::object; + if (f.object) + { + a.members(f.members_a, ordered); + b.members(f.members_b, ordered); + if (f.members_a.size() != f.members_b.size()) + { + return false; + } + } + else + { + a.elements(f.elements_a); + b.elements(f.elements_b); + } + stack.push_back(std::move(f)); + } + else if (!(a.scalar() == b.scalar())) // numbers (also of different types), null, boolean + { + return false; + } + } + + // the next pair of values + for (;;) + { + if (stack.empty()) + { + return true; + } + frame& f = stack.back(); + const std::size_t count = f.object ? f.members_a.size() : f.elements_a.size(); + if (f.next == count) + { + stack.pop_back(); + continue; + } + if (f.object) + { + if (!(f.members_a[f.next].first == f.members_b[f.next].first)) + { + return false; + } + a = f.members_a[f.next].second; + b = f.members_b[f.next].second; + } + else + { + a = f.elements_a[f.next]; + b = f.elements_b[f.next]; + } + ++f.next; + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/materialize.hpp b/include/nlohmann/detail/view/materialize.hpp index e9ffe0668..79fafee1b 100644 --- a/include/nlohmann/detail/view/materialize.hpp +++ b/include/nlohmann/detail/view/materialize.hpp @@ -81,7 +81,7 @@ BasicJsonType materialize(const document_data& d, const node* n) ++n; break; case value_t::number_float: - sax.number_float(float_value(d.str(*n), *n), no_token); + sax.number_float(float_value(d, *n), no_token); ++n; break; case value_t::boolean: diff --git a/include/nlohmann/detail/view/number.hpp b/include/nlohmann/detail/view/number.hpp index c9befdd49..24a87a7b8 100644 --- a/include/nlohmann/detail/view/number.hpp +++ b/include/nlohmann/detail/view/number.hpp @@ -9,11 +9,15 @@ #pragma once #include // size_t +#include // int64_t, uint64_t #include // string +#include // integral_constant #include +#include #include #include +#include NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -62,6 +66,96 @@ NLOHMANN_VIEW_NOINLINE FloatType float_value(const char* first, const node& n) return convert_float(first, last, dot, mantissa_end); } +/*! +@brief the digits of a float token with at most 19 digits, from its layout + +The digit layout recorded while parsing says where the integer digits, the +fraction digits, and the exponent are, so the digits are read eight at a +time without scanning. + +@param[in] p first character of the token +@param[in] e end of the token +@param[in] limit end of the readable memory (the source text) +*/ +NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept +{ + const bool negative = *p == '-'; + p += negative ? 1 : 0; + std::uint64_t w = parse_upto19(p, int_digits, limit); + p += int_digits; + std::int64_t q = 0; + if (frac_digits != 0) + { + w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit); + p += 1 + frac_digits; + q = -static_cast(frac_digits); + } + if (p != e) + { + // [eE][+-]digits; huge exponents saturate (the parser rejected overflow) + ++p; + const bool exp_negative = *p == '-'; + p += (*p == '-' || *p == '+') ? 1 : 0; + std::int64_t exp_value = 0; + for (; p != e; ++p) + { + if (exp_value < 0x10000000) + { + exp_value = (exp_value * 10) + (*p - '0'); + } + } + q += exp_negative ? -exp_value : exp_value; + } + + float_significand d; + d.w = w; + d.exponent = q; + d.negative = negative; + return d; +} + +/*! +@brief the value of a float token with at most 19 digits, from its layout + +The result is correctly rounded by the lexer's conversion +(detail::decimal_to_float(): Clinger's fast path where both operands are +exact, else the Eisel-Lemire algorithm, which needs no fallback for up to 19 +digits), so it is the value parse() produces. +*/ +template +NLOHMANN_VIEW_ALWAYS_INLINE FloatType layout_float(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept +{ + return decimal_to_float(layout_decimal(p, e, int_digits, frac_digits, limit)); +} + +/// the value of the float token of a node, as parse() converts it; floats and +/// doubles with at most 19 digits are converted from the digit layout +template +FloatType float_value(const document_data& d, const node& n) +{ + return float_value(d, n, std::integral_constant::value> {}); +} + +template +FloatType float_value(const document_data& d, const node& n, std::true_type /*binary32 or binary64*/) +{ + const unsigned int_digits = n.extra & 0xFFu; + const unsigned frac_digits = n.extra >> 8u; + if (NLOHMANN_VIEW_LIKELY(int_digits + frac_digits <= 19)) // (255 marks "many") + { + // (a float token not written by an edit is in the text) + const auto* const first = reinterpret_cast(d.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return layout_float(first, first + n.len, int_digits, frac_digits, reinterpret_cast(d.src + d.size)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + return float_value(d.str(n), n); +} + +template +FloatType float_value(const document_data& d, const node& n, std::false_type /*other*/) +{ + return float_value(d.str(n), n); +} + } // namespace view } // namespace detail NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/serializer.hpp b/include/nlohmann/detail/view/serializer.hpp new file mode 100644 index 000000000..26bfb3225 --- /dev/null +++ b/include/nlohmann/detail/view/serializer.hpp @@ -0,0 +1,419 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // max +#include // array +#include // isfinite +#include // size_t +#include // uint8_t, uint32_t +#include // memcpy, memset +#include // numeric_limits +#include // integral_constant +#include // vector + +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// append-only output buffer: writes through a raw pointer into a string that +/// is resized ahead, and trimmed by finish() +template +class output_buffer +{ + public: + output_buffer(StringType& out, std::size_t estimate) + : m_out(sized(out, estimate)) + , m_pos(&m_out[0]) + , m_end(m_pos + m_out.size()) + {} + + void finish() + { + m_out.resize(static_cast(m_pos - m_out.data())); + } + + NLOHMANN_VIEW_ALWAYS_INLINE void reserve(std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(m_end - m_pos) < n)) + { + grow(n); + } + } + + NLOHMANN_VIEW_ALWAYS_INLINE void put(char c) + { + reserve(1); + *m_pos++ = c; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void put(const char* s, std::size_t n) + { + reserve(n); + std::memcpy(m_pos, s, n); + m_pos += n; + } + + void put_repeated(char c, std::size_t n) + { + reserve(n); + std::memset(m_pos, c, n); + m_pos += n; + } + + private: + static StringType& sized(StringType& out, std::size_t estimate) + { + out.resize((std::max)(estimate, static_cast(64))); + return out; + } + + NLOHMANN_VIEW_NOINLINE void grow(std::size_t n) + { + const auto used = static_cast(m_pos - m_out.data()); + m_out.resize((std::max)(m_out.size() * 2, used + n + 256)); + m_pos = &m_out[0] + used; + m_end = &m_out[0] + m_out.size(); + } + + StringType& m_out; + char* m_pos; + char* m_end; +}; + +/// how the view's dump() writes a value +struct dump_style +{ + bool pretty = false; ///< indent >= 0 + std::size_t indent = 0; ///< characters per level + char indent_char = ' '; + bool ensure_ascii = false; + bool source_numbers = false; ///< copy number tokens from the source +}; + +/*! +@brief write a view's subtree as basic_json::dump() writes the value + +The output of a subtree equals ordered_json::parse(text).dump() of it for +the same arguments (members in document order): strings are escaped by the +same rules, with the library's scanning kernels; floats are written with +the library's conversion; integers are copied from the source, where they +are canonical (except "-0", which parse() reads as 0). The walk is +iterative, so the nesting depth is limited by memory only. +*/ +template +class view_serializer +{ + using string_t = typename BasicJsonType::string_t; + using number_float_t = typename BasicJsonType::number_float_t; + + public: + view_serializer(const document_data& d, string_t& out, std::size_t estimate, const dump_style& style) + : m_doc(d), m_out(out, estimate), m_style(style) + {} + + void dump(const node* root) + { + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + bool first; ///< nothing written yet + }; + std::vector stack; + const node* n = root; + for (;;) + { + // write the value at n + if (is_container(*n)) + { + const bool object = n->kind == static_cast(value_t::object); + if (n->len == 0) + { + m_out.put(object ? "{}" : "[]", 2); + } + else + { + m_out.put(object ? '{' : '['); + stack.push_back(frame{document_data::first_child(n), document_data::child_end(n), object, true}); + } + } + else + { + write_scalar(*n); + } + + // go to the next value: close finished containers, then separate + for (;;) + { + if (stack.empty()) + { + m_out.finish(); + return; + } + frame& f = stack.back(); + if (f.pos == f.end) + { + const bool object = f.object; + stack.pop_back(); + newline(stack.size()); + m_out.put(object ? '}' : ']'); + continue; + } + if (!f.first) + { + m_out.put(','); + } + f.first = false; + newline(stack.size()); + if (f.object) + { + write_string(*f.pos); + if (m_style.pretty) + { + m_out.put(": ", 2); + } + else + { + m_out.put(':'); + } + n = f.pos + 1; + } + else + { + n = f.pos; + } + f.pos = document_data::after(n); + break; + } + } + } + + private: + void newline(std::size_t level) + { + if (m_style.pretty) + { + m_out.put('\n'); + m_out.put_repeated(m_style.indent_char, level * m_style.indent); + } + } + + void write_scalar(const node& n) + { + switch (static_cast(n.kind)) + { + case value_t::null: + m_out.put("null", 4); + break; + case value_t::boolean: + if ((n.flags & node_flags::is_true) != 0) + { + m_out.put("true", 4); + } + else + { + m_out.put("false", 5); + } + break; + case value_t::string: + write_string(n); + break; + case value_t::number_integer: + case value_t::number_unsigned: + { + const char* const token = m_doc.str(n); + const std::uint32_t len = number_length(n); + if (!m_style.source_numbers && len == 2 && token[0] == '-' && token[1] == '0') + { + m_out.put('0'); // parse() reads -0 as the integer 0 + } + else + { + m_out.put(token, len); + } + break; + } + case value_t::number_float: + if (m_style.source_numbers) + { + m_out.put(m_doc.str(n), n.len); + } + else + { + write_float(float_value(m_doc, n)); + } + break; + case value_t::object: // LCOV_EXCL_LINE (containers are written by dump()) + case value_t::array: // LCOV_EXCL_LINE + case value_t::binary: // LCOV_EXCL_LINE (not in a document) + case value_t::discarded: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + break; // LCOV_EXCL_LINE + } + } + + /// as serializer::dump_float() + void write_float(number_float_t x) + { + if (!std::isfinite(x)) + { + m_out.put("null", 4); + return; + } + write_float(x, std::integral_constant < bool, + (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 24 && std::numeric_limits::max_exponent == 128) + || (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 53 && std::numeric_limits::max_exponent == 1024) > {}); + } + + void write_float(number_float_t x, std::true_type /*is_ieee_single_or_double*/) + { + std::array buf{}; + const char* const end = ::nlohmann::detail::to_chars(buf.data(), buf.data() + buf.size(), x); + m_out.put(buf.data(), static_cast(end - buf.data())); + } + + void write_float(number_float_t x, std::false_type /*is_ieee_single_or_double*/) + { + // other types (e.g. long double) are rare: the library writes them + const string_t s = BasicJsonType(x).dump(); + m_out.put(s.data(), s.size()); + } + + void write_string(const node& n) + { + const char* const s = m_doc.str(n); + m_out.put('"'); + if ((n.flags & node_flags::escaped) == 0 && !m_style.ensure_ascii) + { + // a string without escape sequences has nothing to escape + m_out.put(s, n.len); + } + else if (m_style.ensure_ascii) + { + write_escaped(reinterpret_cast(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + else + { + write_escaped(reinterpret_cast(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + m_out.put('"'); + } + + /// as serializer::dump_escaped() for valid UTF-8 (the view has no other) + template + void write_escaped(const unsigned char* s, std::size_t n) + { + std::size_t i = 0; + while (i < n) + { + std::size_t run = 0; + if (!EnsureAscii) + { + run = string_bulk_run(s + i, n - i); + } + else if (is_ascii_copyable(s[i])) + { + run = find_ascii_copyable_run(s + i, n - i); + } + if (run != 0) + { + m_out.put(reinterpret_cast(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + i += run; + continue; + } + std::uint32_t codepoint = s[i]; + std::size_t len = 1; + if (codepoint >= 0xC0) + { + len = 2; + if (codepoint >= 0xE0) + { + len = codepoint >= 0xF0 ? 4 : 3; + } + codepoint &= 0xFFu >> (len + 1); + for (std::size_t k = 1; k < len; ++k) + { + codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu); + } + } + write_codepoint(codepoint, s + i, len); + i += len; + } + } + + template + void write_codepoint(std::uint32_t codepoint, const unsigned char* bytes, std::size_t len) + { + switch (codepoint) + { + case 0x08: + m_out.put("\\b", 2); + return; + case 0x09: + m_out.put("\\t", 2); + return; + case 0x0A: + m_out.put("\\n", 2); + return; + case 0x0C: + m_out.put("\\f", 2); + return; + case 0x0D: + m_out.put("\\r", 2); + return; + case 0x22: + m_out.put("\\\"", 2); + return; + case 0x5C: + m_out.put("\\\\", 2); + return; + default: + break; + } + if (codepoint <= 0x1F || (EnsureAscii && codepoint >= 0x7F)) + { + if (codepoint <= 0xFFFF) + { + write_u_escape(codepoint); + } + else + { + write_u_escape(0xD7C0u + (codepoint >> 10u)); + write_u_escape(0xDC00u + (codepoint & 0x3FFu)); + } + return; + } + m_out.put(reinterpret_cast(bytes), len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) LCOV_EXCL_LINE (printable characters are copied in runs) + } + + void write_u_escape(std::uint32_t u) + { + static constexpr const char* hex = "0123456789abcdef"; + const std::array e = {{'\\', 'u', hex[(u >> 12u) & 0xFu], hex[(u >> 8u) & 0xFu], hex[(u >> 4u) & 0xFu], hex[u & 0xFu]}}; + m_out.put(e.data(), e.size()); + } + + const document_data& m_doc; + output_buffer m_out; + const dump_style m_style; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/value.hpp b/include/nlohmann/detail/view/value.hpp index b43a9c4df..23d291634 100644 --- a/include/nlohmann/detail/view/value.hpp +++ b/include/nlohmann/detail/view/value.hpp @@ -49,7 +49,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod case value_t::number_integer: return static_cast(static_cast(static_cast(integer_bits(n)))); case value_t::number_float: - return static_cast(float_value(d.str(n), n)); + return static_cast(float_value(d, n)); case value_t::boolean: return static_cast((n.flags & node_flags::is_true) != 0); case value_t::null: diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index b51ff446b..882101336 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -29,6 +29,9 @@ #include // distance, input_iterator_tag, iterator_traits #include // map #include // unique_ptr +#ifndef JSON_NO_IO + #include // ostream +#endif #include // string #include // tuple_element, tuple_size #include // decay, enable_if, integral_constant, is_arithmetic, is_base_of, is_integral, is_same, remove_cv, remove_extent @@ -44,6 +47,7 @@ #endif #include +#include #include #include #include @@ -53,6 +57,7 @@ #include #include #include +#include #include #include @@ -547,6 +552,96 @@ class basic_json_view return {m_doc->str(*m_node), detail::view::number_length(*m_node)}; } + /////////////////// + // serialization // + /////////////////// + + /// how dump() writes numbers + enum class number_format + { + /// as basic_json::dump(): integers canonically, floats with the + /// library's shortest round-trip digits ("1.5", "100.0", "1e+100") + shortest, + /// the number text of the source as it is ("1.50", "1E2", "-0", all + /// digits of a long integer) + source, + }; + + /// the text of this value; with number_format::shortest, the output of + /// ordered_json::parse(text).dump() with the same arguments (members in + /// document order, all of them should a key occur more than once) + string_t dump(const int indent = -1, const char indent_char = ' ', const bool ensure_ascii = false, + const number_format numbers = number_format::shortest) const + { + string_t out; + if (m_node == nullptr) + { + out = ""; // as basic_json::dump() of a discarded value + return out; + } + detail::view::dump_style style; + style.pretty = indent >= 0; + style.indent = indent >= 0 ? static_cast(indent) : 0; + style.indent_char = indent_char; + style.ensure_ascii = ensure_ascii; + style.source_numbers = numbers == number_format::source; + // the compact text is about as long as the source text of the value + const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 64; + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + return out; + } + +#ifndef JSON_NO_IO + /// as operator<< of basic_json: a stream width > 0 is the indentation, + /// the fill character the indentation character + friend std::ostream& operator<<(std::ostream& o, const basic_json_view& v) + { + const bool pretty = o.width() > 0; + const auto indentation = pretty ? o.width() : 0; + o.width(0); + const string_t s = v.dump(pretty ? static_cast(indentation) : -1, o.fill()); + return o.write(s.data(), static_cast(s.size())); + } +#endif + + //////////////// + // comparison // + //////////////// + + /// whether the values parse() would produce for two views are equal, as + /// by BasicJsonType's operator== (numbers by value, objects by their + /// members with duplicate keys resolved as parse() resolves them) + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), side(b)); + } + + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + + /// whether the value parse() would produce for a view equals a value + friend bool operator==(const basic_json_view& a, const BasicJsonType& j) + { + return detail::view::equal(side(a), json_side_t(j)); + } + + friend bool operator==(const BasicJsonType& j, const basic_json_view& a) + { + return a == j; + } + + friend bool operator!=(const basic_json_view& a, const BasicJsonType& j) + { + return !(a == j); + } + + friend bool operator!=(const BasicJsonType& j, const basic_json_view& a) + { + return !(a == j); + } + ///////////////// // materialize // ///////////////// @@ -579,6 +674,30 @@ class basic_json_view : m_doc(d), m_node(n) {} + using json_side_t = detail::view::json_side; + + static detail::view::view_side side(const basic_json_view& v) noexcept + { + return detail::view::view_side(v); + } + + /// the number of source bytes of this value (estimated for values with + /// decoded strings) + std::size_t source_extent() const noexcept + { + const node* const next = document_data::after(m_node); + const bool in_source = (m_node->flags & detail::view::node_flags::storage) == 0; + if (!in_source) + { + return m_node->len; + } + if (next != m_doc->tape + m_doc->tape_size && (next->flags & detail::view::node_flags::storage) == 0 && next->off >= m_node->off) + { + return next->off - m_node->off; + } + return m_doc->size - m_node->off; + } + /// 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 aefe84c6a..cdec6523d 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -29,6 +29,9 @@ #include // distance, input_iterator_tag, iterator_traits #include // map #include // unique_ptr +#ifndef JSON_NO_IO + #include // ostream +#endif #include // string #include // tuple_element, tuple_size #include // decay, enable_if, integral_constant, is_arithmetic, is_base_of, is_integral, is_same, remove_cv, remove_extent @@ -1575,6 +1578,326 @@ inline bool build(document_data& d, const char* src, std::size_t size, bool comm } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // sort, stable_sort +#include // size_t +#include // string +#include // move, pair +#include // vector + +// #include +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +// Equality of views, and of views with basic_json values, with the semantics +// of basic_json's operator== applied to the values parse() would produce: +// numbers compare by value across their types, an object is compared by its +// members with duplicate keys resolved as parse() resolves them (the last +// value, at the position of the first occurrence), and in document order if +// the object type keeps an order (ordered_json), by key otherwise. + +/// one side of a comparison: a view +template +class view_side +{ + public: + using string_view_t = typename View::string_view_t; + + explicit view_side(const View& v) noexcept + : m_view(v) + {} + + value_t type() const noexcept + { + return m_view.type(); + } + + std::size_t size() const noexcept + { + return m_view.size(); + } + + string_view_t string() const + { + return m_view.get_string(); + } + + /// a number, boolean, or null as a basic_json value (no allocation) + BasicJsonType scalar() const + { + switch (m_view.type()) + { + case value_t::number_integer: + return BasicJsonType(m_view.template get()); + case value_t::number_unsigned: + return BasicJsonType(m_view.template get()); + case value_t::number_float: + return BasicJsonType(m_view.template get()); + case value_t::boolean: + return BasicJsonType(m_view.template get()); + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + case value_t::discarded: + default: + return BasicJsonType(nullptr); + } + } + + void elements(std::vector& out) const + { + out.reserve(m_view.size()); + for (const View e : m_view) + { + out.emplace_back(e); + } + } + + /// the members as parse() keeps them: one per key, the last value at the + /// position of the first occurrence; in that order, or sorted by key + void members(std::vector>& out, bool ordered) const + { + struct member + { + string_view_t key; + View value; + std::size_t position; + }; + std::vector all; + all.reserve(m_view.size()); + std::size_t position = 0; + for (auto it = m_view.begin(); it != m_view.end(); ++it) + { + all.push_back(member{it.key(), it.value(), position++}); + } + std::stable_sort(all.begin(), all.end(), [](const member & a, const member & b) + { + return a.key < b.key; + }); + std::vector unique; + unique.reserve(all.size()); + for (std::size_t i = 0; i < all.size();) + { + std::size_t last = i; + while (last + 1 < all.size() && all[last + 1].key == all[i].key) + { + ++last; + } + unique.push_back(member{all[i].key, all[last].value, all[i].position}); + i = last + 1; + } + if (ordered) + { + std::sort(unique.begin(), unique.end(), [](const member & a, const member & b) + { + return a.position < b.position; + }); + } + out.reserve(unique.size()); + for (const member& m : unique) + { + out.emplace_back(m.key, view_side(m.value)); + } + } + + private: + View m_view; +}; + +/// the other side of a comparison: a basic_json value +template +class json_side +{ + public: + using string_view_t = StringView; + + explicit json_side(const BasicJsonType& j) noexcept + : m_json(&j) + {} + + value_t type() const noexcept + { + return m_json->type(); + } + + std::size_t size() const noexcept + { + return m_json->size(); + } + + string_view_t string() const + { + const auto& s = m_json->template get_ref(); + return string_view_t(s.data(), s.size()); + } + + BasicJsonType scalar() const + { + return *m_json; + } + + void elements(std::vector& out) const + { + out.reserve(m_json->size()); + for (const auto& e : *m_json) + { + out.emplace_back(e); + } + } + + void members(std::vector>& out, bool ordered) const + { + out.reserve(m_json->size()); + for (auto it = m_json->cbegin(); it != m_json->cend(); ++it) + { + out.emplace_back(string_view_t(it.key().data(), it.key().size()), json_side(it.value())); + } + if (!ordered) + { + std::sort(out.begin(), out.end(), [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + } + } + + private: + const BasicJsonType* m_json; +}; + +/// whether two sides are equal; iterative, so that the nesting depth is +/// limited by memory only +template +bool equal(const A& a0, const B& b0) +{ + using string_view_t = typename A::string_view_t; + const bool ordered = is_ordered_map::value; + + struct frame + { + std::vector elements_a{}; + std::vector elements_b{}; + std::vector> members_a{}; + std::vector> members_b{}; + bool object = false; + std::size_t next = 0; + }; + std::vector stack; + A a = a0; + B b = b0; + for (;;) + { + const value_t ta = a.type(); + const value_t tb = b.type(); + const bool numbers = (ta == value_t::number_integer || ta == value_t::number_unsigned || ta == value_t::number_float) + && (tb == value_t::number_integer || tb == value_t::number_unsigned || tb == value_t::number_float); + if (ta == value_t::discarded || tb == value_t::discarded) + { + // basic_json decides (JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON) + if (ta != tb || !(BasicJsonType(value_t::discarded) == BasicJsonType(value_t::discarded))) + { + return false; + } + } + else + { + if (!numbers && ta != tb) + { + return false; + } + if (ta == value_t::string) + { + if (!(a.string() == b.string())) + { + return false; + } + } + else if (ta == value_t::array || ta == value_t::object) + { + if (a.size() != b.size() && ta == value_t::array) + { + return false; + } + frame f; + f.object = ta == value_t::object; + if (f.object) + { + a.members(f.members_a, ordered); + b.members(f.members_b, ordered); + if (f.members_a.size() != f.members_b.size()) + { + return false; + } + } + else + { + a.elements(f.elements_a); + b.elements(f.elements_b); + } + stack.push_back(std::move(f)); + } + else if (!(a.scalar() == b.scalar())) // numbers (also of different types), null, boolean + { + return false; + } + } + + // the next pair of values + for (;;) + { + if (stack.empty()) + { + return true; + } + frame& f = stack.back(); + const std::size_t count = f.object ? f.members_a.size() : f.elements_a.size(); + if (f.next == count) + { + stack.pop_back(); + continue; + } + if (f.object) + { + if (!(f.members_a[f.next].first == f.members_b[f.next].first)) + { + return false; + } + a = f.members_a[f.next].second; + b = f.members_b[f.next].second; + } + else + { + a = f.elements_a[f.next]; + b = f.elements_b[f.next]; + } + ++f.next; + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include // #include @@ -2212,13 +2535,19 @@ NLOHMANN_JSON_NAMESPACE_END #include // size_t +#include // int64_t, uint64_t #include // string +#include // integral_constant // #include +// #include + // #include // #include +// #include + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -2267,6 +2596,96 @@ NLOHMANN_VIEW_NOINLINE FloatType float_value(const char* first, const node& n) return convert_float(first, last, dot, mantissa_end); } +/*! +@brief the digits of a float token with at most 19 digits, from its layout + +The digit layout recorded while parsing says where the integer digits, the +fraction digits, and the exponent are, so the digits are read eight at a +time without scanning. + +@param[in] p first character of the token +@param[in] e end of the token +@param[in] limit end of the readable memory (the source text) +*/ +NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept +{ + const bool negative = *p == '-'; + p += negative ? 1 : 0; + std::uint64_t w = parse_upto19(p, int_digits, limit); + p += int_digits; + std::int64_t q = 0; + if (frac_digits != 0) + { + w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit); + p += 1 + frac_digits; + q = -static_cast(frac_digits); + } + if (p != e) + { + // [eE][+-]digits; huge exponents saturate (the parser rejected overflow) + ++p; + const bool exp_negative = *p == '-'; + p += (*p == '-' || *p == '+') ? 1 : 0; + std::int64_t exp_value = 0; + for (; p != e; ++p) + { + if (exp_value < 0x10000000) + { + exp_value = (exp_value * 10) + (*p - '0'); + } + } + q += exp_negative ? -exp_value : exp_value; + } + + float_significand d; + d.w = w; + d.exponent = q; + d.negative = negative; + return d; +} + +/*! +@brief the value of a float token with at most 19 digits, from its layout + +The result is correctly rounded by the lexer's conversion +(detail::decimal_to_float(): Clinger's fast path where both operands are +exact, else the Eisel-Lemire algorithm, which needs no fallback for up to 19 +digits), so it is the value parse() produces. +*/ +template +NLOHMANN_VIEW_ALWAYS_INLINE FloatType layout_float(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept +{ + return decimal_to_float(layout_decimal(p, e, int_digits, frac_digits, limit)); +} + +/// the value of the float token of a node, as parse() converts it; floats and +/// doubles with at most 19 digits are converted from the digit layout +template +FloatType float_value(const document_data& d, const node& n) +{ + return float_value(d, n, std::integral_constant::value> {}); +} + +template +FloatType float_value(const document_data& d, const node& n, std::true_type /*binary32 or binary64*/) +{ + const unsigned int_digits = n.extra & 0xFFu; + const unsigned frac_digits = n.extra >> 8u; + if (NLOHMANN_VIEW_LIKELY(int_digits + frac_digits <= 19)) // (255 marks "many") + { + // (a float token not written by an edit is in the text) + const auto* const first = reinterpret_cast(d.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return layout_float(first, first + n.len, int_digits, frac_digits, reinterpret_cast(d.src + d.size)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + return float_value(d.str(n), n); +} + +template +FloatType float_value(const document_data& d, const node& n, std::false_type /*other*/) +{ + return float_value(d.str(n), n); +} + } // namespace view } // namespace detail NLOHMANN_JSON_NAMESPACE_END @@ -2335,7 +2754,7 @@ BasicJsonType materialize(const document_data& d, const node* n) ++n; break; case value_t::number_float: - sax.number_float(float_value(d.str(*n), *n), no_token); + sax.number_float(float_value(d, *n), no_token); ++n; break; case value_t::boolean: @@ -2561,6 +2980,431 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // max +#include // array +#include // isfinite +#include // size_t +#include // uint8_t, uint32_t +#include // memcpy, memset +#include // numeric_limits +#include // integral_constant +#include // vector + +// #include +// #include + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// append-only output buffer: writes through a raw pointer into a string that +/// is resized ahead, and trimmed by finish() +template +class output_buffer +{ + public: + output_buffer(StringType& out, std::size_t estimate) + : m_out(sized(out, estimate)) + , m_pos(&m_out[0]) + , m_end(m_pos + m_out.size()) + {} + + void finish() + { + m_out.resize(static_cast(m_pos - m_out.data())); + } + + NLOHMANN_VIEW_ALWAYS_INLINE void reserve(std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(m_end - m_pos) < n)) + { + grow(n); + } + } + + NLOHMANN_VIEW_ALWAYS_INLINE void put(char c) + { + reserve(1); + *m_pos++ = c; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void put(const char* s, std::size_t n) + { + reserve(n); + std::memcpy(m_pos, s, n); + m_pos += n; + } + + void put_repeated(char c, std::size_t n) + { + reserve(n); + std::memset(m_pos, c, n); + m_pos += n; + } + + private: + static StringType& sized(StringType& out, std::size_t estimate) + { + out.resize((std::max)(estimate, static_cast(64))); + return out; + } + + NLOHMANN_VIEW_NOINLINE void grow(std::size_t n) + { + const auto used = static_cast(m_pos - m_out.data()); + m_out.resize((std::max)(m_out.size() * 2, used + n + 256)); + m_pos = &m_out[0] + used; + m_end = &m_out[0] + m_out.size(); + } + + StringType& m_out; + char* m_pos; + char* m_end; +}; + +/// how the view's dump() writes a value +struct dump_style +{ + bool pretty = false; ///< indent >= 0 + std::size_t indent = 0; ///< characters per level + char indent_char = ' '; + bool ensure_ascii = false; + bool source_numbers = false; ///< copy number tokens from the source +}; + +/*! +@brief write a view's subtree as basic_json::dump() writes the value + +The output of a subtree equals ordered_json::parse(text).dump() of it for +the same arguments (members in document order): strings are escaped by the +same rules, with the library's scanning kernels; floats are written with +the library's conversion; integers are copied from the source, where they +are canonical (except "-0", which parse() reads as 0). The walk is +iterative, so the nesting depth is limited by memory only. +*/ +template +class view_serializer +{ + using string_t = typename BasicJsonType::string_t; + using number_float_t = typename BasicJsonType::number_float_t; + + public: + view_serializer(const document_data& d, string_t& out, std::size_t estimate, const dump_style& style) + : m_doc(d), m_out(out, estimate), m_style(style) + {} + + void dump(const node* root) + { + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + bool first; ///< nothing written yet + }; + std::vector stack; + const node* n = root; + for (;;) + { + // write the value at n + if (is_container(*n)) + { + const bool object = n->kind == static_cast(value_t::object); + if (n->len == 0) + { + m_out.put(object ? "{}" : "[]", 2); + } + else + { + m_out.put(object ? '{' : '['); + stack.push_back(frame{document_data::first_child(n), document_data::child_end(n), object, true}); + } + } + else + { + write_scalar(*n); + } + + // go to the next value: close finished containers, then separate + for (;;) + { + if (stack.empty()) + { + m_out.finish(); + return; + } + frame& f = stack.back(); + if (f.pos == f.end) + { + const bool object = f.object; + stack.pop_back(); + newline(stack.size()); + m_out.put(object ? '}' : ']'); + continue; + } + if (!f.first) + { + m_out.put(','); + } + f.first = false; + newline(stack.size()); + if (f.object) + { + write_string(*f.pos); + if (m_style.pretty) + { + m_out.put(": ", 2); + } + else + { + m_out.put(':'); + } + n = f.pos + 1; + } + else + { + n = f.pos; + } + f.pos = document_data::after(n); + break; + } + } + } + + private: + void newline(std::size_t level) + { + if (m_style.pretty) + { + m_out.put('\n'); + m_out.put_repeated(m_style.indent_char, level * m_style.indent); + } + } + + void write_scalar(const node& n) + { + switch (static_cast(n.kind)) + { + case value_t::null: + m_out.put("null", 4); + break; + case value_t::boolean: + if ((n.flags & node_flags::is_true) != 0) + { + m_out.put("true", 4); + } + else + { + m_out.put("false", 5); + } + break; + case value_t::string: + write_string(n); + break; + case value_t::number_integer: + case value_t::number_unsigned: + { + const char* const token = m_doc.str(n); + const std::uint32_t len = number_length(n); + if (!m_style.source_numbers && len == 2 && token[0] == '-' && token[1] == '0') + { + m_out.put('0'); // parse() reads -0 as the integer 0 + } + else + { + m_out.put(token, len); + } + break; + } + case value_t::number_float: + if (m_style.source_numbers) + { + m_out.put(m_doc.str(n), n.len); + } + else + { + write_float(float_value(m_doc, n)); + } + break; + case value_t::object: // LCOV_EXCL_LINE (containers are written by dump()) + case value_t::array: // LCOV_EXCL_LINE + case value_t::binary: // LCOV_EXCL_LINE (not in a document) + case value_t::discarded: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + break; // LCOV_EXCL_LINE + } + } + + /// as serializer::dump_float() + void write_float(number_float_t x) + { + if (!std::isfinite(x)) + { + m_out.put("null", 4); + return; + } + write_float(x, std::integral_constant < bool, + (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 24 && std::numeric_limits::max_exponent == 128) + || (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 53 && std::numeric_limits::max_exponent == 1024) > {}); + } + + void write_float(number_float_t x, std::true_type /*is_ieee_single_or_double*/) + { + std::array buf{}; + const char* const end = ::nlohmann::detail::to_chars(buf.data(), buf.data() + buf.size(), x); + m_out.put(buf.data(), static_cast(end - buf.data())); + } + + void write_float(number_float_t x, std::false_type /*is_ieee_single_or_double*/) + { + // other types (e.g. long double) are rare: the library writes them + const string_t s = BasicJsonType(x).dump(); + m_out.put(s.data(), s.size()); + } + + void write_string(const node& n) + { + const char* const s = m_doc.str(n); + m_out.put('"'); + if ((n.flags & node_flags::escaped) == 0 && !m_style.ensure_ascii) + { + // a string without escape sequences has nothing to escape + m_out.put(s, n.len); + } + else if (m_style.ensure_ascii) + { + write_escaped(reinterpret_cast(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + else + { + write_escaped(reinterpret_cast(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + m_out.put('"'); + } + + /// as serializer::dump_escaped() for valid UTF-8 (the view has no other) + template + void write_escaped(const unsigned char* s, std::size_t n) + { + std::size_t i = 0; + while (i < n) + { + std::size_t run = 0; + if (!EnsureAscii) + { + run = string_bulk_run(s + i, n - i); + } + else if (is_ascii_copyable(s[i])) + { + run = find_ascii_copyable_run(s + i, n - i); + } + if (run != 0) + { + m_out.put(reinterpret_cast(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + i += run; + continue; + } + std::uint32_t codepoint = s[i]; + std::size_t len = 1; + if (codepoint >= 0xC0) + { + len = 2; + if (codepoint >= 0xE0) + { + len = codepoint >= 0xF0 ? 4 : 3; + } + codepoint &= 0xFFu >> (len + 1); + for (std::size_t k = 1; k < len; ++k) + { + codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu); + } + } + write_codepoint(codepoint, s + i, len); + i += len; + } + } + + template + void write_codepoint(std::uint32_t codepoint, const unsigned char* bytes, std::size_t len) + { + switch (codepoint) + { + case 0x08: + m_out.put("\\b", 2); + return; + case 0x09: + m_out.put("\\t", 2); + return; + case 0x0A: + m_out.put("\\n", 2); + return; + case 0x0C: + m_out.put("\\f", 2); + return; + case 0x0D: + m_out.put("\\r", 2); + return; + case 0x22: + m_out.put("\\\"", 2); + return; + case 0x5C: + m_out.put("\\\\", 2); + return; + default: + break; + } + if (codepoint <= 0x1F || (EnsureAscii && codepoint >= 0x7F)) + { + if (codepoint <= 0xFFFF) + { + write_u_escape(codepoint); + } + else + { + write_u_escape(0xD7C0u + (codepoint >> 10u)); + write_u_escape(0xDC00u + (codepoint & 0x3FFu)); + } + return; + } + m_out.put(reinterpret_cast(bytes), len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) LCOV_EXCL_LINE (printable characters are copied in runs) + } + + void write_u_escape(std::uint32_t u) + { + static constexpr const char* hex = "0123456789abcdef"; + const std::array e = {{'\\', 'u', hex[(u >> 12u) & 0xFu], hex[(u >> 8u) & 0xFu], hex[(u >> 4u) & 0xFu], hex[u & 0xFu]}}; + m_out.put(e.data(), e.size()); + } + + const document_data& m_doc; + output_buffer m_out; + const dump_style m_style; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -2734,7 +3578,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod case value_t::number_integer: return static_cast(static_cast(static_cast(integer_bits(n)))); case value_t::number_float: - return static_cast(float_value(d.str(n), n)); + return static_cast(float_value(d, n)); case value_t::boolean: return static_cast((n.flags & node_flags::is_true) != 0); case value_t::null: @@ -3284,6 +4128,96 @@ class basic_json_view return {m_doc->str(*m_node), detail::view::number_length(*m_node)}; } + /////////////////// + // serialization // + /////////////////// + + /// how dump() writes numbers + enum class number_format + { + /// as basic_json::dump(): integers canonically, floats with the + /// library's shortest round-trip digits ("1.5", "100.0", "1e+100") + shortest, + /// the number text of the source as it is ("1.50", "1E2", "-0", all + /// digits of a long integer) + source, + }; + + /// the text of this value; with number_format::shortest, the output of + /// ordered_json::parse(text).dump() with the same arguments (members in + /// document order, all of them should a key occur more than once) + string_t dump(const int indent = -1, const char indent_char = ' ', const bool ensure_ascii = false, + const number_format numbers = number_format::shortest) const + { + string_t out; + if (m_node == nullptr) + { + out = ""; // as basic_json::dump() of a discarded value + return out; + } + detail::view::dump_style style; + style.pretty = indent >= 0; + style.indent = indent >= 0 ? static_cast(indent) : 0; + style.indent_char = indent_char; + style.ensure_ascii = ensure_ascii; + style.source_numbers = numbers == number_format::source; + // the compact text is about as long as the source text of the value + const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 64; + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + return out; + } + +#ifndef JSON_NO_IO + /// as operator<< of basic_json: a stream width > 0 is the indentation, + /// the fill character the indentation character + friend std::ostream& operator<<(std::ostream& o, const basic_json_view& v) + { + const bool pretty = o.width() > 0; + const auto indentation = pretty ? o.width() : 0; + o.width(0); + const string_t s = v.dump(pretty ? static_cast(indentation) : -1, o.fill()); + return o.write(s.data(), static_cast(s.size())); + } +#endif + + //////////////// + // comparison // + //////////////// + + /// whether the values parse() would produce for two views are equal, as + /// by BasicJsonType's operator== (numbers by value, objects by their + /// members with duplicate keys resolved as parse() resolves them) + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), side(b)); + } + + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + + /// whether the value parse() would produce for a view equals a value + friend bool operator==(const basic_json_view& a, const BasicJsonType& j) + { + return detail::view::equal(side(a), json_side_t(j)); + } + + friend bool operator==(const BasicJsonType& j, const basic_json_view& a) + { + return a == j; + } + + friend bool operator!=(const basic_json_view& a, const BasicJsonType& j) + { + return !(a == j); + } + + friend bool operator!=(const BasicJsonType& j, const basic_json_view& a) + { + return !(a == j); + } + ///////////////// // materialize // ///////////////// @@ -3316,6 +4250,30 @@ class basic_json_view : m_doc(d), m_node(n) {} + using json_side_t = detail::view::json_side; + + static detail::view::view_side side(const basic_json_view& v) noexcept + { + return detail::view::view_side(v); + } + + /// the number of source bytes of this value (estimated for values with + /// decoded strings) + std::size_t source_extent() const noexcept + { + const node* const next = document_data::after(m_node); + const bool in_source = (m_node->flags & detail::view::node_flags::storage) == 0; + if (!in_source) + { + return m_node->len; + } + if (next != m_doc->tape + m_doc->tape_size && (next->flags & detail::view::node_flags::storage) == 0 && next->off >= m_node->off) + { + return next->off - m_node->off; + } + return m_doc->size - m_node->off; + } + /// 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/benchmarks/README.md b/tests/benchmarks/README.md index e329a6c8b..7148ecb94 100644 --- a/tests/benchmarks/README.md +++ b/tests/benchmarks/README.md @@ -21,6 +21,7 @@ Micro-benchmarks for parsing, serialization and the binary formats, written with | `ViewParseIndented` | as `ParseIndented`, with a reused `json_document` | | `ViewAccept` | validate with `json_document::accept`; compare with `Accept` | | `ViewMaterialize` | convert a parsed `json_document` into a `json` value | +| `ViewDump` | serialize a parsed `json_document`; compare with `Dump` | The input files are those of [nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark) (`canada`, `citm_catalog`, `twitter`), a large `jeopardy` file, and number-heavy files (`floats`, `signed_ints`, ...). diff --git a/tests/benchmarks/src/benchmarks_view.cpp b/tests/benchmarks/src/benchmarks_view.cpp index d6d04d1d5..475ed54a4 100644 --- a/tests/benchmarks/src/benchmarks_view.cpp +++ b/tests/benchmarks/src/benchmarks_view.cpp @@ -144,3 +144,37 @@ static void ViewMaterialize(benchmark::State& state, const char* filename) state.SetBytesProcessed(state.iterations() * str.size()); } JSON_VIEW_BENCHMARK_FILES(ViewMaterialize); + +////////////////////////////////////////////////////////////////////////////// +// serialize a parsed document (compare with Dump) +////////////////////////////////////////////////////////////////////////////// + +static void ViewDump(benchmark::State& state, const char* filename, int indent) +{ + const std::string str = read_file(filename); + const json_document d = json_document::parse(str); + + while (state.KeepRunning()) + { + std::string output = d.root().dump(indent); + benchmark::DoNotOptimize(output); + } + + state.SetBytesProcessed(state.iterations() * d.root().dump(indent).size()); +} +BENCHMARK_CAPTURE(ViewDump, jeopardy / -, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json", -1); +BENCHMARK_CAPTURE(ViewDump, jeopardy / 4, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json", 4); +BENCHMARK_CAPTURE(ViewDump, canada / -, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", -1); +BENCHMARK_CAPTURE(ViewDump, canada / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", 4); +BENCHMARK_CAPTURE(ViewDump, citm_catalog / -, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", -1); +BENCHMARK_CAPTURE(ViewDump, citm_catalog / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", 4); +BENCHMARK_CAPTURE(ViewDump, twitter / -, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", -1); +BENCHMARK_CAPTURE(ViewDump, twitter / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", 4); +BENCHMARK_CAPTURE(ViewDump, floats / -, TEST_DATA_DIRECTORY "/regression/floats.json", -1); +BENCHMARK_CAPTURE(ViewDump, floats / 4, TEST_DATA_DIRECTORY "/regression/floats.json", 4); +BENCHMARK_CAPTURE(ViewDump, signed_ints / -, TEST_DATA_DIRECTORY "/regression/signed_ints.json", -1); +BENCHMARK_CAPTURE(ViewDump, signed_ints / 4, TEST_DATA_DIRECTORY "/regression/signed_ints.json", 4); +BENCHMARK_CAPTURE(ViewDump, unsigned_ints / -, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json", -1); +BENCHMARK_CAPTURE(ViewDump, unsigned_ints / 4, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json", 4); +BENCHMARK_CAPTURE(ViewDump, small_signed_ints / -, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json", -1); +BENCHMARK_CAPTURE(ViewDump, small_signed_ints / 4, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json", 4); diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index e208c2abb..cea863c66 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -22,6 +22,7 @@ using nlohmann::ordered_json_view; #include #include #include +#include #include #include #include @@ -872,7 +873,15 @@ TEST_CASE("json_view values") std::mt19937_64 rng(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) std::vector tokens = {"0.1", "-0.0", "1e308", "1.7976931348623157e308", "2.2250738585072011e-308", "4.9e-324", "5e-324", "0.1000000000000000055511151231257827021181583404541015625", "123456789012345678901234567890", - "9007199254740993", "1.00000000000000011102230246251565404236316680908203125", "7.2057594037927933e16" + "9007199254740993", "1.00000000000000011102230246251565404236316680908203125", "7.2057594037927933e16", + // around the limits of the conversion from the digit layout: 19 and 20 + // digits, and those of Clinger's fast path (2^53, 10^22) + "1234567890.123456789", "1234567890.1234567891", "0.0000000000000000001", "123456789012345678.9", + "9007199254740992.0", "9007199254740993.0", "9007199254740994.0", "1.5e22", "1.5e23", "15e-22", "15e-23", + "1e-400", "0.0e0", "-0.0e-5", "12E+3", "12e-0", + // and of float: 2^24 + 1 and 2^24 + 3 (ties), the subnormal and normal limits + "16777217", "16777219", "1.4e-45", "7.006492321624085e-46", "7.006492321624086e-46", + "1.17549435e-38", "0.30000001192092896", "3.4028234e37" }; for (int i = 0; i < 20000; ++i) { @@ -1081,3 +1090,190 @@ TEST_CASE("json_view JSON pointers") } #endif } + +TEST_CASE("json_view dump") +{ + SECTION("the output of ordered_json::dump()") + { + generator g; + for (int i = 0; i < 2000; ++i) + { + std::string text; + g.value(text, 0); + const ordered_json_document d = ordered_json_document::parse(text); + if (has_duplicate_keys(d.root())) + { + continue; + } + CAPTURE(text) + const ordered_json j = ordered_json::parse(text); + for (const int indent : + { + -1, 0, 2 + }) + { + for (const bool ensure_ascii : + { + false, true + }) + { + CHECK(d.root().dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii) == j.dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii)); + } + } + // also of each element + for (const ordered_json_view e : d.root()) + { + CHECK(e.dump() == e.materialize().dump()); + } + } + } + + SECTION("strings") + { + const std::string text = R"(["plain", "\u0000\u0001\u001f\u007f\u0080é€￿😀", "\"\\\/\b\f\n\r\t", "aéあ😀b", "long text beyond the eight bytes of a word \n with an escape in the middle"])"; + const ordered_json_document d = ordered_json_document::parse(text); + const ordered_json j = ordered_json::parse(text); + CHECK(d.root().dump() == j.dump()); + CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true)); + CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true)); + const ordered_json_document keys = ordered_json_document::parse(R"({"é\n": {"\"": [], "": {}}})"); + CHECK(keys.root().dump(2, ' ', true) == ordered_json::parse(R"({"é\n": {"\"": [], "": {}}})").dump(2, ' ', true)); + } + + SECTION("numbers") + { + const std::string text = "[1.50, 1E2, -0, -0.0, 123456789012345678901234567890, 18446744073709551615, -9223372036854775808, 0.1, 1e-7, 5e-324]"; + const json_document d = json_document::parse(text); + CHECK(d.root().dump() == json::parse(text).dump()); + CHECK(d.root().dump() == "[1.5,100.0,0,-0.0,1.2345678901234568e+29,18446744073709551615,-9223372036854775808,0.1,1e-07,5e-324]"); + CHECK(d.root().dump(-1, ' ', false, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]"); + + // random doubles, written as parse() and dump() would + std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + std::string many = "["; + for (int i = 0; i < 5000; ++i) + { + const std::uint64_t bits = rng(); + double x = 0; + std::memcpy(&x, &bits, sizeof(x)); + if (std::isfinite(x)) + { + many += (many.size() > 1 ? "," : "") + json(x).dump(); + } + } + many += ']'; + CHECK(json_document::parse(many).root().dump() == json::parse(many).dump()); + + using json_float = nlohmann::basic_json; + CHECK(nlohmann::basic_json_document::parse("[0.1, 1.5e10, 3.4028235e38]").root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump()); + } + + SECTION("members in document order, all of them") + { + 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}"); + } + + SECTION("deep nesting") + { + const std::string deep = std::string(100000, '[') + std::string(100000, ']'); + CHECK(json_document::parse(deep).root().dump() == deep); + } + + SECTION("streams and discarded views") + { + const json_document d = json_document::parse(R"({"a": [1, 2]})"); + std::ostringstream compact; + compact << d.root(); + CHECK(compact.str() == R"({"a":[1,2]})"); + std::ostringstream pretty; + pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"]; + CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]"); + CHECK(json_view().dump() == json(json::value_t::discarded).dump()); + } +} + +TEST_CASE("json_view comparison") +{ + SECTION("equality of the values parse() produces") + { + generator g; + std::vector texts; + for (int i = 0; i < 600; ++i) + { + std::string text; + g.value(text, 0); + texts.push_back(text); + // the same value written differently: sorted keys, canonical numbers + texts.push_back(json::parse(text).dump(1)); + } + for (std::size_t i = 0; i + 2 < texts.size(); ++i) + { + for (std::size_t k = i; k < i + 3; ++k) + { + CAPTURE(texts[i]) + CAPTURE(texts[k]) + const json_document a = json_document::parse(texts[i]); + const json_document b = json_document::parse(texts[k]); + const json ja = json::parse(texts[i]); + const json jb = json::parse(texts[k]); + CHECK((a.root() == b.root()) == (ja == jb)); + CHECK((a.root() != b.root()) == (ja != jb)); + CHECK((a.root() == jb) == (ja == jb)); + CHECK((jb == a.root()) == (ja == jb)); + CHECK((a.root() != jb) == (ja != jb)); + CHECK((jb != a.root()) == (ja != jb)); + + // ordered_json compares members in order + const ordered_json_document oa = ordered_json_document::parse(texts[i]); + const ordered_json_document ob = ordered_json_document::parse(texts[k]); + const ordered_json oja = ordered_json::parse(texts[i]); + const ordered_json ojb = ordered_json::parse(texts[k]); + CHECK((oa.root() == ob.root()) == (oja == ojb)); + CHECK((oa.root() == ojb) == (oja == ojb)); + } + } + } + + SECTION("numbers, duplicate keys, member order") + { + const auto same = [](const char* x, const char* y) + { + return json_document::parse(x).root() == json_document::parse(y).root(); + }; + CHECK(same("1", "1.0")); + CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]")); + CHECK(!same("1", "1.5")); + CHECK(same("18446744073709551615", "18446744073709551615")); + CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})")); + CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})")); + CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})")); + CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})")); + CHECK(!same("[1, 2]", "[2, 1]")); + CHECK(!same("\"a\"", "\"b\"")); + CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\"")); + CHECK(!same("null", "false")); + CHECK(!same("[]", "{}")); + CHECK(ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})").root() == ordered_json_document::parse(R"({"a": 3, "b": 2})").root()); + CHECK(ordered_json_document::parse(R"({"a": 1, "b": 2})").root() != ordered_json_document::parse(R"({"b": 2, "a": 1})").root()); + + // discarded values compare as basic_json's do + const json discarded(json::value_t::discarded); + CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested + CHECK((json_view() == discarded) == (discarded == discarded)); + CHECK(!(json_view() == json_document::parse("null").root())); // NOLINT(readability-container-size-empty) + CHECK(!(json_document::parse("null").root() == discarded)); + } + + SECTION("deep nesting") + { + const std::string deep = std::string(100000, '[') + std::string(100000, ']'); + const json_document a = json_document::parse(deep); + const json_document b = json_document::parse(deep); + CHECK(a.root() == b.root()); + CHECK(a.root() == json::parse(deep)); + const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']'); + CHECK(a.root() != json_document::parse(other).root()); + } +}