Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Hash/Indexable#dig/dig? #6719

Merged
merged 3 commits into from
Sep 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions spec/std/hash_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
48 changes: 48 additions & 0 deletions spec/std/indexable_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
40 changes: 40 additions & 0 deletions spec/std/json/any_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions spec/std/named_tuple_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions spec/std/yaml/any_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
39 changes: 39 additions & 0 deletions src/hash.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
#
# ```
Expand Down
39 changes: 39 additions & 0 deletions src/indexable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
#
Expand Down
26 changes: 26 additions & 0 deletions src/json/any.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading