Skip to content

Commit 01f1927

Browse files
committed
Implement a new JSON::try_at with a start hint
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 1d572ae commit 01f1927

3 files changed

Lines changed: 178 additions & 0 deletions

File tree

src/core/json/include/sourcemeta/core/json_object.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,35 @@ template <typename Key, typename Value, typename Hash> class JSONObject {
270270
return nullptr;
271271
}
272272

273+
/// Try to access an object entry, scanning from a caller-provided start
274+
/// offset. On hit, advances `start` past the found index
275+
[[nodiscard]] inline auto try_at(const Key &key, const hash_type key_hash,
276+
size_type &start) const
277+
-> const mapped_type * {
278+
assert(this->hash(key) == key_hash);
279+
const auto object_size{this->size()};
280+
if (this->hasher.is_perfect(key_hash)) {
281+
for (size_type count = 0; count < object_size; count++) {
282+
const auto index{(start + count) % object_size};
283+
if (this->data[index].hash == key_hash) {
284+
start = index + 1;
285+
return &this->data[index].second;
286+
}
287+
}
288+
} else {
289+
for (size_type count = 0; count < object_size; count++) {
290+
const auto index{(start + count) % object_size};
291+
if (this->data[index].hash == key_hash &&
292+
this->data[index].first == key) {
293+
start = index + 1;
294+
return &this->data[index].second;
295+
}
296+
}
297+
}
298+
299+
return nullptr;
300+
}
301+
273302
/// Try to emplace a property before another property
274303
auto try_emplace_before(const Key &key, const mapped_type &value,
275304
const Key &suffix) -> hash_type {

src/core/json/include/sourcemeta/core/json_value.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,38 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON {
12821282
return object.try_at(key, hash);
12831283
}
12841284

1285+
/// Try to get a property, scanning from a caller-provided start offset.
1286+
/// On hit, advances `start` past the found index. When looking up multiple
1287+
/// keys in insertion order, each lookup hits on the first probe, making the
1288+
/// total work O(N) instead of O(N^2). For example:
1289+
///
1290+
/// ```cpp
1291+
/// #include <sourcemeta/core/json.h>
1292+
/// #include <cassert>
1293+
///
1294+
/// const sourcemeta::core::JSON document =
1295+
/// sourcemeta::core::parse_json("{ \"foo\": 1, \"bar\": 2 }");
1296+
/// const auto &object{document.as_object()};
1297+
/// typename decltype(object)::size_type start{0};
1298+
///
1299+
/// const auto hash_foo{object.hash("foo")};
1300+
/// const auto *foo{document.try_at("foo", hash_foo, start)};
1301+
/// assert(foo);
1302+
/// assert(foo->to_integer() == 1);
1303+
///
1304+
/// const auto hash_bar{object.hash("bar")};
1305+
/// const auto *bar{document.try_at("bar", hash_bar, start)};
1306+
/// assert(bar);
1307+
/// assert(bar->to_integer() == 2);
1308+
/// ```
1309+
[[nodiscard]] SOURCEMETA_FORCEINLINE inline auto
1310+
try_at(const String &key, const typename Object::hash_type hash,
1311+
typename Object::size_type &start) const -> const JSON * {
1312+
assert(this->is_object());
1313+
const auto &object{this->data_object};
1314+
return object.try_at(key, hash, start);
1315+
}
1316+
12851317
/// This method checks whether an input JSON object defines a specific key.
12861318
/// For example:
12871319
///

test/json/json_object_test.cc

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,3 +1093,120 @@ TEST(JSON_object, reorder_already_ordered) {
10931093
EXPECT_EQ(iterator->first, "cherry");
10941094
EXPECT_EQ(iterator->second.to_integer(), 3);
10951095
}
1096+
1097+
TEST(JSON_object, try_at_start_hit) {
1098+
const sourcemeta::core::JSON document =
1099+
sourcemeta::core::parse_json("{\"foo\":1,\"bar\":2,\"baz\":3}");
1100+
const auto &object{document.as_object()};
1101+
sourcemeta::core::JSON::Object::size_type start{0};
1102+
1103+
const auto hash_foo{object.hash("foo")};
1104+
const auto *result_foo{object.try_at("foo", hash_foo, start)};
1105+
EXPECT_TRUE(result_foo);
1106+
EXPECT_EQ(result_foo->to_integer(), 1);
1107+
EXPECT_EQ(start, 1);
1108+
1109+
const auto hash_bar{object.hash("bar")};
1110+
const auto *result_bar{object.try_at("bar", hash_bar, start)};
1111+
EXPECT_TRUE(result_bar);
1112+
EXPECT_EQ(result_bar->to_integer(), 2);
1113+
EXPECT_EQ(start, 2);
1114+
1115+
const auto hash_baz{object.hash("baz")};
1116+
const auto *result_baz{object.try_at("baz", hash_baz, start)};
1117+
EXPECT_TRUE(result_baz);
1118+
EXPECT_EQ(result_baz->to_integer(), 3);
1119+
EXPECT_EQ(start, 3);
1120+
}
1121+
1122+
TEST(JSON_object, try_at_start_miss) {
1123+
const sourcemeta::core::JSON document =
1124+
sourcemeta::core::parse_json("{\"foo\":1,\"bar\":2}");
1125+
const auto &object{document.as_object()};
1126+
sourcemeta::core::JSON::Object::size_type start{0};
1127+
1128+
const auto hash_missing{object.hash("missing")};
1129+
const auto *result{object.try_at("missing", hash_missing, start)};
1130+
EXPECT_FALSE(result);
1131+
EXPECT_EQ(start, 0);
1132+
}
1133+
1134+
TEST(JSON_object, try_at_start_reverse_order) {
1135+
const sourcemeta::core::JSON document =
1136+
sourcemeta::core::parse_json("{\"foo\":1,\"bar\":2,\"baz\":3}");
1137+
const auto &object{document.as_object()};
1138+
sourcemeta::core::JSON::Object::size_type start{0};
1139+
1140+
const auto hash_baz{object.hash("baz")};
1141+
const auto *result_baz{object.try_at("baz", hash_baz, start)};
1142+
EXPECT_TRUE(result_baz);
1143+
EXPECT_EQ(result_baz->to_integer(), 3);
1144+
EXPECT_EQ(start, 3);
1145+
1146+
const auto hash_bar{object.hash("bar")};
1147+
const auto *result_bar{object.try_at("bar", hash_bar, start)};
1148+
EXPECT_TRUE(result_bar);
1149+
EXPECT_EQ(result_bar->to_integer(), 2);
1150+
EXPECT_EQ(start, 2);
1151+
1152+
const auto hash_foo{object.hash("foo")};
1153+
const auto *result_foo{object.try_at("foo", hash_foo, start)};
1154+
EXPECT_TRUE(result_foo);
1155+
EXPECT_EQ(result_foo->to_integer(), 1);
1156+
EXPECT_EQ(start, 1);
1157+
}
1158+
1159+
TEST(JSON_object, try_at_start_single_property) {
1160+
const sourcemeta::core::JSON document =
1161+
sourcemeta::core::parse_json("{\"only\":42}");
1162+
const auto &object{document.as_object()};
1163+
sourcemeta::core::JSON::Object::size_type start{0};
1164+
1165+
const auto hash_only{object.hash("only")};
1166+
const auto *result{object.try_at("only", hash_only, start)};
1167+
EXPECT_TRUE(result);
1168+
EXPECT_EQ(result->to_integer(), 42);
1169+
EXPECT_EQ(start, 1);
1170+
}
1171+
1172+
TEST(JSON_object, try_at_start_wraparound) {
1173+
const sourcemeta::core::JSON document =
1174+
sourcemeta::core::parse_json("{\"foo\":1,\"bar\":2,\"baz\":3}");
1175+
const auto &object{document.as_object()};
1176+
sourcemeta::core::JSON::Object::size_type start{10};
1177+
1178+
const auto hash_foo{object.hash("foo")};
1179+
const auto *result{object.try_at("foo", hash_foo, start)};
1180+
EXPECT_TRUE(result);
1181+
EXPECT_EQ(result->to_integer(), 1);
1182+
EXPECT_EQ(start, 1);
1183+
}
1184+
1185+
TEST(JSON_object, try_at_start_sequential_advances) {
1186+
const sourcemeta::core::JSON document =
1187+
sourcemeta::core::parse_json("{\"a\":1,\"b\":2,\"c\":3,\"d\":4}");
1188+
const auto &object{document.as_object()};
1189+
sourcemeta::core::JSON::Object::size_type start{0};
1190+
1191+
const auto hash_a{object.hash("a")};
1192+
const auto *result_a{object.try_at("a", hash_a, start)};
1193+
EXPECT_TRUE(result_a);
1194+
EXPECT_EQ(result_a->to_integer(), 1);
1195+
1196+
const auto hash_b{object.hash("b")};
1197+
const auto *result_b{object.try_at("b", hash_b, start)};
1198+
EXPECT_TRUE(result_b);
1199+
EXPECT_EQ(result_b->to_integer(), 2);
1200+
1201+
const auto hash_c{object.hash("c")};
1202+
const auto *result_c{object.try_at("c", hash_c, start)};
1203+
EXPECT_TRUE(result_c);
1204+
EXPECT_EQ(result_c->to_integer(), 3);
1205+
1206+
const auto hash_d{object.hash("d")};
1207+
const auto *result_d{object.try_at("d", hash_d, start)};
1208+
EXPECT_TRUE(result_d);
1209+
EXPECT_EQ(result_d->to_integer(), 4);
1210+
1211+
EXPECT_EQ(start, object.size());
1212+
}

0 commit comments

Comments
 (0)