diff --git a/spec/std/hash_spec.cr b/spec/std/hash_spec.cr index 4984b0cd4a44..94d5add7896a 100644 --- a/spec/std/hash_spec.cr +++ b/spec/std/hash_spec.cr @@ -105,6 +105,51 @@ describe "Hash" do end end + describe "dig?" do + it "gets the value at given path given splat" do + ary = [1, 2, 3] + h = {"a" => {"b" => {"c" => [10, 20]}}, ary => {"a" => "b"}} + + h.dig?("a", "b", "c").should eq([10, 20]) + h.dig?(ary, "a").should eq("b") + end + + it "returns nil if not found" do + ary = [1, 2, 3] + h = {"a" => {"b" => {"c" => 300}}, ary => {"a" => "b"}} + + h.dig?("a", "b", "c", "d", "e").should be_nil + h.dig?("z").should be_nil + h.dig?("").should be_nil + end + end + + describe "dig" do + it "gets the value at given path given splat" do + ary = [1, 2, 3] + h = {"a" => {"b" => {"c" => [10, 20]}}, ary => {"a" => "b", "c" => nil}} + + h.dig("a", "b", "c").should eq([10, 20]) + h.dig(ary, "a").should eq("b") + h.dig(ary, "c").should eq(nil) + end + + it "raises KeyError if not found" do + ary = [1, 2, 3] + h = {"a" => {"b" => {"c" => 300}}, ary => {"a" => "b"}} + + expect_raises KeyError, %(Hash value not diggable for key: "c") do + h.dig("a", "b", "c", "d", "e") + end + expect_raises KeyError, %(Missing hash key: "z") do + h.dig("z") + end + expect_raises KeyError, %(Missing hash key: "") do + h.dig("") + end + end + end + describe "fetch" do it "fetches with one argument" do a = {1 => 2} diff --git a/spec/std/indexable_spec.cr b/spec/std/indexable_spec.cr index a000be09fc75..5a57febda770 100644 --- a/spec/std/indexable_spec.cr +++ b/spec/std/indexable_spec.cr @@ -42,6 +42,24 @@ private class SafeMixedIndexable end end +private class SafeRecursiveIndexable + include Indexable(SafeRecursiveIndexable | Int32) + + property size + + def initialize(@size : Int32) + end + + def unsafe_at(i) + raise IndexError.new unless 0 <= i < size + if (i % 2) == 0 + SafeRecursiveIndexable.new(i) + else + i + end + end +end + describe Indexable do it "does index with big negative offset" do indexable = SafeIndexable.new(3) @@ -166,4 +184,34 @@ describe Indexable do indexable.join(", ").should eq("0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11") indexable.join(98).should eq("098198298398498598698798898998109811") end + + describe "dig?" do + it "gets the value at given path given splat" do + indexable = SafeRecursiveIndexable.new(30) + indexable.dig?(20, 10, 4, 3).should eq(3) + end + + it "returns nil if not found" do + indexable = SafeRecursiveIndexable.new(30) + indexable.dig?(2, 4).should be_nil + indexable.dig?(3, 7).should be_nil + end + end + + describe "dig" do + it "gets the value at given path given splat" do + indexable = SafeRecursiveIndexable.new(30) + indexable.dig(20, 10, 4, 3).should eq(3) + end + + it "raises IndexError if not found" do + indexable = SafeRecursiveIndexable.new(30) + expect_raises IndexError, %(Index out of bounds) do + indexable.dig(2, 4) + end + expect_raises IndexError, %(Indexable value not diggable for index: 3) do + indexable.dig(3, 7) + end + end + end end diff --git a/spec/std/json/any_spec.cr b/spec/std/json/any_spec.cr index ec033b62c62e..cd692f8c81b0 100644 --- a/spec/std/json/any_spec.cr +++ b/spec/std/json/any_spec.cr @@ -86,6 +86,46 @@ describe JSON::Any do end end + describe "#dig?" do + it "gets the value at given path given splat" do + obj = JSON.parse(%({"foo": [1, {"bar": [2, 3]}]})) + + obj.dig?("foo", 0).should eq(1) + obj.dig?("foo", 1, "bar", 1).should eq(3) + end + + it "returns nil if not found" do + obj = JSON.parse(%({"foo": [1, {"bar": [2, 3]}]})) + + obj.dig?("foo", 10).should be_nil + obj.dig?("bar", "baz").should be_nil + obj.dig?("").should be_nil + end + end + + describe "dig" do + it "gets the value at given path given splat" do + obj = JSON.parse(%({"foo": [1, {"bar": [2, 3]}]})) + + obj.dig("foo", 0).should eq(1) + obj.dig("foo", 1, "bar", 1).should eq(3) + end + + it "raises if not found" do + obj = JSON.parse(%({"foo": [1, {"bar": [2, 3]}]})) + + expect_raises Exception, %(Expected Hash for #[](key : String), not Array(JSON::Any)) do + obj.dig("foo", 1, "bar", "baz") + end + expect_raises KeyError, %(Missing hash key: "z") do + obj.dig("z") + end + expect_raises KeyError, %(Missing hash key: "") do + obj.dig("") + end + end + end + it "traverses big structure" do obj = JSON.parse(%({"foo": [1, {"bar": [2, 3]}]})) obj["foo"][1]["bar"][1].as_i.should eq(3) diff --git a/spec/std/named_tuple_spec.cr b/spec/std/named_tuple_spec.cr index 28de9b8f1ffe..2a5bf8eee11e 100644 --- a/spec/std/named_tuple_spec.cr +++ b/spec/std/named_tuple_spec.cr @@ -133,6 +133,47 @@ describe "NamedTuple" do typeof(val).should eq(Int32 | Char | Nil) end + describe "dig?" do + it "gets the value at given path given splat" do + h = {a: {b: {c: [10, 20]}}, x: {a: "b"}} + + h.dig?(:a, :b, :c).should eq([10, 20]) + h.dig?("x", "a").should eq("b") + end + + it "returns nil if not found" do + h = {a: {b: {c: 300}}, x: {a: "b"}} + + h.dig?("a", "b", "c", "d", "e").should be_nil + h.dig?("z").should be_nil + h.dig?("").should be_nil + end + end + + describe "dig" do + it "gets the value at given path given splat" do + h = {a: {b: {c: [10, 20]}}, x: {a: "b", c: nil}} + + h.dig(:a, :b, :c).should eq([10, 20]) + h.dig("x", "a").should eq("b") + h.dig("x", "c").should eq(nil) + end + + it "raises KeyError if not found" do + h = {a: {b: {c: 300}}, x: {a: "b"}} + + expect_raises KeyError, %(NamedTuple value not diggable for key: "c") do + h.dig("a", "b", "c", "d", "e") + end + expect_raises KeyError, %(Missing named tuple key: "z") do + h.dig("z") + end + expect_raises KeyError, %(Missing named tuple key: "") do + h.dig("") + end + end + end + it "computes a hash value" do tup1 = {a: 1, b: 'a'} tup1.hash.should eq(tup1.dup.hash) diff --git a/spec/std/yaml/any_spec.cr b/spec/std/yaml/any_spec.cr index 1933c64100e6..0609e6ca7c14 100644 --- a/spec/std/yaml/any_spec.cr +++ b/spec/std/yaml/any_spec.cr @@ -128,6 +128,46 @@ describe YAML::Any do end end + describe "#dig?" do + it "gets the value at given path given splat" do + obj = YAML.parse("--- \nfoo: \n bar: \n baz: \n - qux\n - fox") + + obj.dig?("foo", "bar", "baz").should eq(%w(qux fox)) + obj.dig?("foo", "bar", "baz", 1).should eq("fox") + end + + it "returns nil if not found" do + obj = YAML.parse("--- \nfoo: \n bar: \n baz: \n - qux\n - fox") + + obj.dig?("foo", 10).should be_nil + obj.dig?("bar", "baz").should be_nil + obj.dig?("").should be_nil + end + end + + describe "dig" do + it "gets the value at given path given splat" do + obj = YAML.parse("--- \nfoo: \n bar: \n baz: \n - qux\n - fox") + + obj.dig("foo", "bar", "baz").should eq(%w(qux fox)) + obj.dig("foo", "bar", "baz", 1).should eq("fox") + end + + it "raises if not found" do + obj = YAML.parse("--- \nfoo: \n bar: \n baz: \n - qux\n - fox") + + expect_raises KeyError, %(Missing hash key: 1) do + obj.dig("foo", 1, "bar", "baz") + end + expect_raises KeyError, %(Missing hash key: "bar") do + obj.dig("bar", "baz") + end + expect_raises KeyError, %(Missing hash key: "") do + obj.dig("") + end + end + end + it "traverses big structure" do obj = YAML.parse("--- \nfoo: \n bar: \n baz: \n - qux\n - fox") obj["foo"]["bar"]["baz"][1].as_s.should eq("fox") diff --git a/src/hash.cr b/src/hash.cr index 668bd898f0b3..55e9912d9375 100644 --- a/src/hash.cr +++ b/src/hash.cr @@ -77,6 +77,45 @@ class Hash(K, V) fetch(key, nil) end + # Traverses the depth of a structure and returns the value. + # Returns `nil` if not found. + # + # ``` + # h = {"a" => {"b" => [10, 20, 30]}} + # h.dig? "a", "b" # => [10, 20, 30] + # h.dig? "a", "b", "c", "d", "e" # => nil + # ``` + def dig?(key : K, *subkeys) + if (value = self[key]?) && value.responds_to?(:dig?) + value.dig?(*subkeys) + end + end + + # :nodoc: + def dig?(key : K) + self[key]? + end + + # Traverses the depth of a structure and returns the value, otherwise + # raises `KeyError`. + # + # ``` + # h = {"a" => {"b" => [10, 20, 30]}} + # h.dig "a", "b" # => [10, 20, 30] + # h.dig "a", "b", "c", "d", "e" # raises KeyError + # ``` + def dig(key : K, *subkeys) + if (value = self[key]) && value.responds_to?(:dig) + return value.dig(*subkeys) + end + raise KeyError.new "Hash value not diggable for key: #{key.inspect}" + end + + # :nodoc: + def dig(key : K) + self[key] + end + # Returns `true` when key given by *key* exists, otherwise `false`. # # ``` diff --git a/src/indexable.cr b/src/indexable.cr index 157e6eebd813..2730f7f311f4 100644 --- a/src/indexable.cr +++ b/src/indexable.cr @@ -93,6 +93,45 @@ module Indexable(T) at(index) { nil } end + # Traverses the depth of a structure and returns the value. + # Returns `nil` if not found. + # + # ``` + # ary = [{1, 2, 3, {4, 5, 6}}] + # ary.dig?(0, 3, 2) # => 6 + # ary.dig?(0, 3, 3) # => nil + # ``` + def dig?(index : Int, *subindexes) + if (value = self[index]?) && value.responds_to?(:dig?) + value.dig?(*subindexes) + end + end + + # :nodoc: + def dig?(index : Int) + self[index]? + end + + # Traverses the depth of a structure and returns the value, otherwise + # raises `IndexError`. + # + # ``` + # ary = [{1, 2, 3, {4, 5, 6}}] + # ary.dig(0, 3, 2) # => 6 + # ary.dig(0, 3, 3) # raises IndexError + # ``` + def dig(index : Int, *subindexes) + if (value = self[index]) && value.responds_to?(:dig) + return value.dig(*subindexes) + end + raise IndexError.new "Indexable value not diggable for index: #{index.inspect}" + end + + # :nodoc: + def dig(index : Int) + self[index] + end + # By using binary search, returns the first element # for which the passed block returns `true`. # diff --git a/src/json/any.cr b/src/json/any.cr index 9d52d085cc0d..edcce0e98a27 100644 --- a/src/json/any.cr +++ b/src/json/any.cr @@ -115,6 +115,32 @@ struct JSON::Any end end + # Traverses the depth of a structure and returns the value. + # Returns `nil` if not found. + def dig?(key : String | Int, *subkeys) + if (value = self[key]?) && value.responds_to?(:dig?) + value.dig?(*subkeys) + end + end + + # :nodoc: + def dig?(key : String | Int) + self[key]? + end + + # Traverses the depth of a structure and returns the value, otherwise raises. + def dig(key : String | Int, *subkeys) + if (value = self[key]) && value.responds_to?(:dig) + return value.dig(*subkeys) + end + raise "JSON::Any value not diggable for key: #{key.inspect}" + end + + # :nodoc: + def dig(key : String | Int) + self[key] + end + # Checks that the underlying value is `Nil`, and returns `nil`. # Raises otherwise. def as_nil : Nil diff --git a/src/named_tuple.cr b/src/named_tuple.cr index 85180395ca01..e948bac66994 100644 --- a/src/named_tuple.cr +++ b/src/named_tuple.cr @@ -119,6 +119,45 @@ struct NamedTuple fetch(key, nil) end + # Traverses the depth of a structure and returns the value. + # Returns `nil` if not found. + # + # ``` + # h = {a: {b: [10, 20, 30]}} + # h.dig? "a", "b" # => [10, 20, 30] + # h.dig? "a", "b", "c", "d", "e" # => nil + # ``` + def dig?(key : Symbol | String, *subkeys) + if (value = self[key]?) && value.responds_to?(:dig?) + value.dig?(*subkeys) + end + end + + # :nodoc: + def dig?(key : Symbol | String) + self[key]? + end + + # Traverses the depth of a structure and returns the value, otherwise + # raises `KeyError`. + # + # ``` + # h = {a: {b: [10, 20, 30]}} + # h.dig "a", "b" # => [10, 20, 30] + # h.dig "a", "b", "c", "d", "e" # raises KeyError + # ``` + def dig(key : Symbol | String, *subkeys) + if (value = self[key]) && value.responds_to?(:dig) + return value.dig(*subkeys) + end + raise KeyError.new "NamedTuple value not diggable for key: #{key.inspect}" + end + + # :nodoc: + def dig(key : Symbol | String) + self[key] + end + # Returns the value for the given *key*, if there's such key, otherwise returns *default_value*. # # ``` diff --git a/src/yaml/any.cr b/src/yaml/any.cr index 97def02a7857..4b59b4b29222 100644 --- a/src/yaml/any.cr +++ b/src/yaml/any.cr @@ -126,6 +126,32 @@ struct YAML::Any end end + # Traverses the depth of a structure and returns the value. + # Returns `nil` if not found. + def dig?(index_or_key, *subkeys) + if (value = self[index_or_key]?) && value.responds_to?(:dig?) + value.dig?(*subkeys) + end + end + + # :nodoc: + def dig?(index_or_key) + self[index_or_key]? + end + + # Traverses the depth of a structure and returns the value, otherwise raises. + def dig(index_or_key, *subkeys) + if (value = self[index_or_key]) && value.responds_to?(:dig) + return value.dig(*subkeys) + end + raise "YAML::Any value not diggable for key: #{index_or_key.inspect}" + end + + # :nodoc: + def dig(index_or_key) + self[index_or_key] + end + # Checks that the underlying value is `Nil`, and returns `nil`. # Raises otherwise. def as_nil : Nil