diff --git a/changes/belt-hashmap-get-with-default.feature.md b/changes/belt-hashmap-get-with-default.feature.md new file mode 100644 index 00000000000..839a9ebfdd0 --- /dev/null +++ b/changes/belt-hashmap-get-with-default.feature.md @@ -0,0 +1 @@ +- Add `getWithDefault` and `getOrThrow` to `Belt.HashMap`, `Belt.HashMap.Int` and `Belt.HashMap.String`. diff --git a/packages/@rescript/belt/src/Belt_HashMap.res b/packages/@rescript/belt/src/Belt_HashMap.res index 340ff3f75fb..65dcfb5499b 100644 --- a/packages/@rescript/belt/src/Belt_HashMap.res +++ b/packages/@rescript/belt/src/Belt_HashMap.res @@ -170,6 +170,40 @@ let get = (h, key) => { } } +let rec getWithDefaultAux = (~eq, key, buckets, default) => + switch C.toOpt(buckets) { + | None => default + | Some(cell) => + if eq(key, cell.N.key) { + cell.N.value + } else { + getWithDefaultAux(~eq, key, cell.N.next, default) + } + } + +let getWithDefault = (h, key, default) => { + let h_buckets = h.C.buckets + let nid = Int.bitwiseAnd(Belt_Id.getHashInternal(h.C.hash)(key), A.length(h_buckets) - 1) + getWithDefaultAux(~eq=Belt_Id.getEqInternal(h.C.eq), key, A.getUnsafe(h_buckets, nid), default) +} + +let rec getOrThrowAux = (~eq, key, buckets) => + switch C.toOpt(buckets) { + | None => throw(Not_found) + | Some(cell) => + if eq(key, cell.N.key) { + cell.N.value + } else { + getOrThrowAux(~eq, key, cell.N.next) + } + } + +let getOrThrow = (h, key) => { + let h_buckets = h.C.buckets + let nid = Int.bitwiseAnd(Belt_Id.getHashInternal(h.C.hash)(key), A.length(h_buckets) - 1) + getOrThrowAux(~eq=Belt_Id.getEqInternal(h.C.eq), key, A.getUnsafe(h_buckets, nid)) +} + let rec memInBucket = (key, cell, ~eq) => eq(cell.N.key, key) || switch C.toOpt(cell.N.next) { diff --git a/packages/@rescript/belt/src/Belt_HashMap.resi b/packages/@rescript/belt/src/Belt_HashMap.resi index 5a69ba4f036..898a861847a 100644 --- a/packages/@rescript/belt/src/Belt_HashMap.resi +++ b/packages/@rescript/belt/src/Belt_HashMap.resi @@ -203,6 +203,52 @@ Belt.HashMap.has(s0, 2) == false */ let has: (t<'key, 'value, 'id>, 'key) => bool +/** +`getWithDefault(tbl, k, default)` returns the value bound to `k`, or `default` if `k` is not bound. + +## Examples + +```rescript +module IntHash = Belt.Id.MakeHashable({ + type t = int + let hash = a => a + let eq = (a, b) => a == b +}) + +let s0 = Belt.HashMap.make(~hintSize=10, ~id=module(IntHash)) +Belt.HashMap.set(s0, 1, "value1") + +Belt.HashMap.getWithDefault(s0, 1, "default") == "value1" +Belt.HashMap.getWithDefault(s0, 2, "default") == "default" +``` +*/ +let getWithDefault: (t<'key, 'value, 'id>, 'key, 'value) => 'value + +/** +`getOrThrow(tbl, k)` returns the value bound to `k`. Throws `Not_found` if `k` is not bound. + +## Examples + +```rescript +module IntHash = Belt.Id.MakeHashable({ + type t = int + let hash = a => a + let eq = (a, b) => a == b +}) + +let s0 = Belt.HashMap.make(~hintSize=10, ~id=module(IntHash)) +Belt.HashMap.set(s0, 1, "value1") + +Belt.HashMap.getOrThrow(s0, 1) == "value1" + +switch Belt.HashMap.getOrThrow(s0, 2) { +| exception Not_found => assert(true) +| _ => assert(false) +} +``` +*/ +let getOrThrow: (t<'key, 'value, 'id>, 'key) => 'value + /** If bound exists, removes it from the hash map. diff --git a/packages/@rescript/belt/src/Belt_HashMapInt.res b/packages/@rescript/belt/src/Belt_HashMapInt.res index 3782945bb62..35e667d22fa 100644 --- a/packages/@rescript/belt/src/Belt_HashMapInt.res +++ b/packages/@rescript/belt/src/Belt_HashMapInt.res @@ -154,6 +154,40 @@ let get = (h, key: key) => { } } +let rec getWithDefaultAux = (key: key, buckets, default) => + switch C.toOpt(buckets) { + | None => default + | Some(cell) => + if key == cell.N.key { + cell.N.value + } else { + getWithDefaultAux(key, cell.N.next, default) + } + } + +let getWithDefault = (h, key: key, default) => { + let h_buckets = h.C.buckets + let nid = Int.bitwiseAnd(hash(key), A.length(h_buckets) - 1) + getWithDefaultAux(key, A.getUnsafe(h_buckets, nid), default) +} + +let rec getOrThrowAux = (key: key, buckets) => + switch C.toOpt(buckets) { + | None => throw(Not_found) + | Some(cell) => + if key == cell.N.key { + cell.N.value + } else { + getOrThrowAux(key, cell.N.next) + } + } + +let getOrThrow = (h, key: key) => { + let h_buckets = h.C.buckets + let nid = Int.bitwiseAnd(hash(key), A.length(h_buckets) - 1) + getOrThrowAux(key, A.getUnsafe(h_buckets, nid)) +} + let rec memInBucket = (key: key, cell) => cell.N.key == key || switch C.toOpt(cell.next) { diff --git a/packages/@rescript/belt/src/Belt_HashMapInt.resi b/packages/@rescript/belt/src/Belt_HashMapInt.resi index 79a4700e0da..b501ad6ba57 100644 --- a/packages/@rescript/belt/src/Belt_HashMapInt.resi +++ b/packages/@rescript/belt/src/Belt_HashMapInt.resi @@ -19,6 +19,38 @@ let get: (t<'a>, key) => option<'a> let has: (t<'b>, key) => bool +/** +`getWithDefault(tbl, k, default)` returns the value bound to `k`, or `default` if `k` is not bound. + +## Examples + +```rescript +let hMap = Belt.HashMap.Int.fromArray([(1, "value1")]) + +Belt.HashMap.Int.getWithDefault(hMap, 1, "default") == "value1" +Belt.HashMap.Int.getWithDefault(hMap, 2, "default") == "default" +``` +*/ +let getWithDefault: (t<'a>, key, 'a) => 'a + +/** +`getOrThrow(tbl, k)` returns the value bound to `k`. Throws `Not_found` if `k` is not bound. + +## Examples + +```rescript +let hMap = Belt.HashMap.Int.fromArray([(1, "value1")]) + +Belt.HashMap.Int.getOrThrow(hMap, 1) == "value1" + +switch Belt.HashMap.Int.getOrThrow(hMap, 2) { +| exception Not_found => assert(true) +| _ => assert(false) +} +``` +*/ +let getOrThrow: (t<'a>, key) => 'a + let remove: (t<'a>, key) => unit @deprecated("Use `forEach` instead") diff --git a/packages/@rescript/belt/src/Belt_HashMapString.res b/packages/@rescript/belt/src/Belt_HashMapString.res index 9e442090c7a..ce8fe026178 100644 --- a/packages/@rescript/belt/src/Belt_HashMapString.res +++ b/packages/@rescript/belt/src/Belt_HashMapString.res @@ -154,6 +154,40 @@ let get = (h, key: key) => { } } +let rec getWithDefaultAux = (key: key, buckets, default) => + switch C.toOpt(buckets) { + | None => default + | Some(cell) => + if key == cell.N.key { + cell.N.value + } else { + getWithDefaultAux(key, cell.N.next, default) + } + } + +let getWithDefault = (h, key: key, default) => { + let h_buckets = h.C.buckets + let nid = Int.bitwiseAnd(hash(key), A.length(h_buckets) - 1) + getWithDefaultAux(key, A.getUnsafe(h_buckets, nid), default) +} + +let rec getOrThrowAux = (key: key, buckets) => + switch C.toOpt(buckets) { + | None => throw(Not_found) + | Some(cell) => + if key == cell.N.key { + cell.N.value + } else { + getOrThrowAux(key, cell.N.next) + } + } + +let getOrThrow = (h, key: key) => { + let h_buckets = h.C.buckets + let nid = Int.bitwiseAnd(hash(key), A.length(h_buckets) - 1) + getOrThrowAux(key, A.getUnsafe(h_buckets, nid)) +} + let rec memInBucket = (key: key, cell) => cell.N.key == key || switch C.toOpt(cell.next) { diff --git a/packages/@rescript/belt/src/Belt_HashMapString.resi b/packages/@rescript/belt/src/Belt_HashMapString.resi index a45b37233cf..98f25ee81f4 100644 --- a/packages/@rescript/belt/src/Belt_HashMapString.resi +++ b/packages/@rescript/belt/src/Belt_HashMapString.resi @@ -19,6 +19,38 @@ let get: (t<'a>, key) => option<'a> let has: (t<'b>, key) => bool +/** +`getWithDefault(tbl, k, default)` returns the value bound to `k`, or `default` if `k` is not bound. + +## Examples + +```rescript +let hMap = Belt.HashMap.String.fromArray([("a", "value1")]) + +Belt.HashMap.String.getWithDefault(hMap, "a", "default") == "value1" +Belt.HashMap.String.getWithDefault(hMap, "b", "default") == "default" +``` +*/ +let getWithDefault: (t<'a>, key, 'a) => 'a + +/** +`getOrThrow(tbl, k)` returns the value bound to `k`. Throws `Not_found` if `k` is not bound. + +## Examples + +```rescript +let hMap = Belt.HashMap.String.fromArray([("a", "value1")]) + +Belt.HashMap.String.getOrThrow(hMap, "a") == "value1" + +switch Belt.HashMap.String.getOrThrow(hMap, "b") { +| exception Not_found => assert(true) +| _ => assert(false) +} +``` +*/ +let getOrThrow: (t<'a>, key) => 'a + let remove: (t<'a>, key) => unit @deprecated("Use `forEach` instead") diff --git a/tests/belt_tests/src/belt_hashmap_test.mjs b/tests/belt_tests/src/belt_hashmap_test.mjs index 0999ac84eda..899edcaf8a0 100644 --- a/tests/belt_tests/src/belt_hashmap_test.mjs +++ b/tests/belt_tests/src/belt_hashmap_test.mjs @@ -9,6 +9,8 @@ import * as Belt_HashMap from "@rescript/belt/src/Belt_HashMap.mjs"; import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.mjs"; import * as Belt_SortArray from "@rescript/belt/src/Belt_SortArray.mjs"; import * as Array_data_util from "./array_data_util.mjs"; +import * as Belt_HashMapInt from "@rescript/belt/src/Belt_HashMapInt.mjs"; +import * as Belt_HashMapString from "@rescript/belt/src/Belt_HashMapString.mjs"; function intEq(x, y) { return x === y; @@ -67,6 +69,71 @@ Mocha.describe("Belt_hashmap_test", () => { Test_utils.eq("File \"belt_hashmap_test.res\", line 43, characters 7-14", Belt_HashMap.size(v), 98000); Test_utils.ok("File \"belt_hashmap_test.res\", line 44, characters 7-14", Belt_Array.every(Array_data_util.range(2001, 100000), x => Belt_HashMap.has(v, x))); }); + Mocha.test("getWithDefault and getOrThrow", () => { + let v = Belt_HashMap.fromArray([ + [ + 1, + 10 + ], + [ + 2, + 20 + ] + ], Y); + Test_utils.eq("File \"belt_hashmap_test.res\", line 49, characters 7-14", Belt_HashMap.getWithDefault(v, 1, 0), 10); + Test_utils.eq("File \"belt_hashmap_test.res\", line 50, characters 7-14", Belt_HashMap.getWithDefault(v, 3, 0), 0); + Test_utils.eq("File \"belt_hashmap_test.res\", line 51, characters 7-14", Belt_HashMap.getOrThrow(v, 2), 20); + Test_utils.throws("File \"belt_hashmap_test.res\", line 52, characters 11-18", () => { + Belt_HashMap.getOrThrow(v, 3); + }); + }); + Mocha.test("getWithDefault and getOrThrow with colliding keys", () => { + let Collide = Belt_Id.hashable(param => 0, intEq); + let v = Belt_HashMap.make(4, Collide); + for (let i = 0; i <= 9; ++i) { + Belt_HashMap.set(v, i, i * 10 | 0); + } + Test_utils.ok("File \"belt_hashmap_test.res\", line 61, characters 7-14", Belt_Array.every(Array_data_util.range(0, 9), i => Belt_HashMap.getWithDefault(v, i, -1) === (i * 10 | 0))); + Test_utils.ok("File \"belt_hashmap_test.res\", line 62, characters 7-14", Belt_Array.every(Array_data_util.range(0, 9), i => Belt_HashMap.getOrThrow(v, i) === (i * 10 | 0))); + Test_utils.eq("File \"belt_hashmap_test.res\", line 63, characters 7-14", Belt_HashMap.getWithDefault(v, 10, -1), -1); + Test_utils.throws("File \"belt_hashmap_test.res\", line 64, characters 11-18", () => { + Belt_HashMap.getOrThrow(v, 10); + }); + }); + Mocha.test("Int and String getWithDefault and getOrThrow", () => { + let i = Belt_HashMapInt.fromArray([ + [ + 1, + "a" + ], + [ + 2, + "b" + ] + ]); + Test_utils.eq("File \"belt_hashmap_test.res\", line 69, characters 7-14", Belt_HashMapInt.getWithDefault(i, 2, "z"), "b"); + Test_utils.eq("File \"belt_hashmap_test.res\", line 70, characters 7-14", Belt_HashMapInt.getWithDefault(i, 3, "z"), "z"); + Test_utils.eq("File \"belt_hashmap_test.res\", line 71, characters 7-14", Belt_HashMapInt.getOrThrow(i, 1), "a"); + Test_utils.throws("File \"belt_hashmap_test.res\", line 72, characters 11-18", () => { + Belt_HashMapInt.getOrThrow(i, 3); + }); + let s = Belt_HashMapString.fromArray([ + [ + "a", + 1 + ], + [ + "b", + 2 + ] + ]); + Test_utils.eq("File \"belt_hashmap_test.res\", line 74, characters 7-14", Belt_HashMapString.getWithDefault(s, "b", 0), 2); + Test_utils.eq("File \"belt_hashmap_test.res\", line 75, characters 7-14", Belt_HashMapString.getWithDefault(s, "c", 0), 0); + Test_utils.eq("File \"belt_hashmap_test.res\", line 76, characters 7-14", Belt_HashMapString.getOrThrow(s, "a"), 1); + Test_utils.throws("File \"belt_hashmap_test.res\", line 77, characters 11-18", () => { + Belt_HashMapString.getOrThrow(s, "c"); + }); + }); }); let N; diff --git a/tests/belt_tests/src/belt_hashmap_test.res b/tests/belt_tests/src/belt_hashmap_test.res index da9f599ff55..743eebad731 100644 --- a/tests/belt_tests/src/belt_hashmap_test.res +++ b/tests/belt_tests/src/belt_hashmap_test.res @@ -43,4 +43,37 @@ describe(__MODULE__, () => { eq(__LOC__, N.size(v), 98_000) ok(__LOC__, A.every(I.range(2_001, 100_000), x => N.has(v, x))) }) + + test("getWithDefault and getOrThrow", () => { + let v = N.fromArray(~id=module(Y), [(1, 10), (2, 20)]) + eq(__LOC__, N.getWithDefault(v, 1, 0), 10) + eq(__LOC__, N.getWithDefault(v, 3, 0), 0) + eq(__LOC__, N.getOrThrow(v, 2), 20) + throws(__LOC__, () => N.getOrThrow(v, 3)->ignore) + }) + + test("getWithDefault and getOrThrow with colliding keys", () => { + module Collide = unpack(Belt.Id.hashable(~eq=intEq, ~hash=(_: int) => 0)) + let v = N.make(~id=module(Collide), ~hintSize=4) + for i in 0 to 9 { + N.set(v, i, i * 10) + } + ok(__LOC__, A.every(I.range(0, 9), i => N.getWithDefault(v, i, -1) == i * 10)) + ok(__LOC__, A.every(I.range(0, 9), i => N.getOrThrow(v, i) == i * 10)) + eq(__LOC__, N.getWithDefault(v, 10, -1), -1) + throws(__LOC__, () => N.getOrThrow(v, 10)->ignore) + }) + + test("Int and String getWithDefault and getOrThrow", () => { + let i = Belt.HashMap.Int.fromArray([(1, "a"), (2, "b")]) + eq(__LOC__, Belt.HashMap.Int.getWithDefault(i, 2, "z"), "b") + eq(__LOC__, Belt.HashMap.Int.getWithDefault(i, 3, "z"), "z") + eq(__LOC__, Belt.HashMap.Int.getOrThrow(i, 1), "a") + throws(__LOC__, () => Belt.HashMap.Int.getOrThrow(i, 3)->ignore) + let s = Belt.HashMap.String.fromArray([("a", 1), ("b", 2)]) + eq(__LOC__, Belt.HashMap.String.getWithDefault(s, "b", 0), 2) + eq(__LOC__, Belt.HashMap.String.getWithDefault(s, "c", 0), 0) + eq(__LOC__, Belt.HashMap.String.getOrThrow(s, "a"), 1) + throws(__LOC__, () => Belt.HashMap.String.getOrThrow(s, "c")->ignore) + }) })