Skip to content

Commit

Permalink
Add Hash#put_if_absent (crystal-lang#13590)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil authored Jul 21, 2023
1 parent 2c9b552 commit c3bf074
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 16 deletions.
2 changes: 1 addition & 1 deletion samples/havlak.cr
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class CFG
property :basic_block_map

def create_node(name)
node = (@basic_block_map[name] ||= BasicBlock.new(name))
node = @basic_block_map.put_if_absent(name) { BasicBlock.new(name) }
@start_node ||= node
node
end
Expand Down
29 changes: 28 additions & 1 deletion spec/std/hash_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe "Hash" do
end
end

describe "put" do
describe "#put" do
it "puts in a small hash" do
a = {} of Int32 => Int32
a.put(1, 2) { nil }.should eq(nil)
Expand All @@ -191,6 +191,33 @@ describe "Hash" do
end
end

describe "#put_if_absent" do
it "puts if key doesn't exist" do
v = [] of String
h = {} of Int32 => Array(String)
h.put_if_absent(1, v).should be(v)
h.should eq({1 => v})
h[1].should be(v)
end

it "returns existing value if key exists" do
v = [] of String
h = {1 => v}
h.put_if_absent(1, [] of String).should be(v)
h.should eq({1 => v})
h[1].should be(v)
end

it "accepts a block" do
v = [] of String
h = {1 => v}
h.put_if_absent(1) { [] of String }.should be(v)
h.put_if_absent(2) { |key| [key.to_s] }.should eq(["2"])
h.should eq({1 => v, 2 => ["2"]})
h[1].should be(v)
end
end

describe "update" do
it "updates the value of an existing key with the given block" do
h = {"a" => 0, "b" => 1}
Expand Down
2 changes: 1 addition & 1 deletion src/csv.cr
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class CSV
headers = @headers = headers.map &.strip
indices = @indices = {} of String => Int32
headers.each_with_index do |header, index|
indices[header] ||= index
indices.put_if_absent(header, index)
end
end
@traversed = false
Expand Down
99 changes: 97 additions & 2 deletions src/hash.cr
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,56 @@ class Hash(K, V)
end
end

# Inserts a key-value pair. Assumes that the given key doesn't exist.
private def insert_new(key, value)
# Unless otherwise noted, this body should be identical to `#upsert`

if @entries.null?
@indices_size_pow2 = 3
@entries = malloc_entries(4)
end

hash = key_hash(key)

if @indices.null?
# don't call `#update_linear_scan` here

if !entries_full?
add_entry_and_increment_size(hash, key, value)
return
end

resize

if @indices.null?
add_entry_and_increment_size(hash, key, value)
return
end
end

index = fit_in_indices(hash)

while true
entry_index = get_index(index)

if entry_index == -1
if entries_full?
resize
index = fit_in_indices(hash)
next
end

set_index(index, entries_size)
add_entry_and_increment_size(hash, key, value)
return
end

# don't call `#get_entry` and `#entry_matches?` here

index = next_index(index)
end
end

# Tries to update a key-value-hash triplet by doing a linear scan.
# Returns an old `Entry` if it was updated, otherwise `nil`.
private def update_linear_scan(key, value, hash) : Entry(K, V)?
Expand Down Expand Up @@ -1003,20 +1053,65 @@ class Hash(K, V)

# Sets the value of *key* to the given *value*.
#
# If a value already exists for `key`, that (old) value is returned.
# If a value already exists for *key*, that (old) value is returned.
# Otherwise the given block is invoked with *key* and its value is returned.
#
# ```
# h = {} of Int32 => String
# h.put(1, "one") { "didn't exist" } # => "didn't exist"
# h.put(1, "uno") { "didn't exist" } # => "one"
# h.put(2, "two") { |key| key.to_s } # => "2"
# h # => {1 => "one", 2 => "two"}
# ```
def put(key : K, value : V, &)
updated_entry = upsert(key, value)
updated_entry ? updated_entry.value : yield key
end

# Sets the value of *key* to the given *value*, unless a value for *key*
# already exists.
#
# If a value already exists for *key*, that (old) value is returned.
# Otherwise *value* is returned.
#
# ```
# h = {} of Int32 => Array(String)
# h.put_if_absent(1, "one") # => "one"
# h.put_if_absent(1, "uno") # => "one"
# h.put_if_absent(2, "two") # => "two"
# h # => {1 => "one", 2 => "two"}
# ```
def put_if_absent(key : K, value : V) : V
put_if_absent(key) { value }
end

# Sets the value of *key* to the value returned by the given block, unless a
# value for *key* already exists.
#
# If a value already exists for *key*, that (old) value is returned.
# Otherwise the given block is invoked with *key* and its value is returned.
#
# ```
# h = {} of Int32 => Array(String)
# h.put_if_absent(1) { |key| [key.to_s] } # => ["1"]
# h.put_if_absent(1) { [] of String } # => ["1"]
# h.put_if_absent(2) { |key| [key.to_s] } # => ["2"]
# h # => {1 => ["1"], 2 => ["2"]}
# ```
#
# `hash.put_if_absent(key) { value }` is a more performant alternative to
# `hash[key] ||= value` that also works correctly when the hash may contain
# falsey values.
def put_if_absent(key : K, & : K -> V) : V
if entry = find_entry(key)
entry.value
else
value = yield key
insert_new(key, value)
value
end
end

# Updates the current value of *key* with the value returned by the given block
# (the current value is used as input for the block).
#
Expand Down Expand Up @@ -1049,7 +1144,7 @@ class Hash(K, V)
entry.value
elsif block = @block
default_value = block.call(self, key)
upsert(key, yield default_value)
insert_new(key, yield default_value)
default_value
else
raise KeyError.new "Missing hash key: #{key.inspect}"
Expand Down
2 changes: 1 addition & 1 deletion src/ini.cr
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ module INI
raise ParseException.new("Data after section", lineno, end_idx + 1) unless end_idx == line.size - 1

current_section_name = line[offset + 1...end_idx]
current_section = ini[current_section_name] ||= Hash(String, String).new
current_section = ini.put_if_absent(current_section_name) { Hash(String, String).new }
else
key, eq, value = line.partition('=')
raise ParseException.new("Expected declaration", lineno, key.size) if eq != "="
Expand Down
3 changes: 1 addition & 2 deletions src/mime.cr
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,7 @@ module MIME
@@types[extension] = type
@@types_lower[extension.downcase] = type

type_extensions = @@extensions[mediatype] ||= Set(String).new
type_extensions << extension
@@extensions.put_if_absent(mediatype) { Set(String).new } << extension
end

# Returns all extensions registered for *type*.
Expand Down
3 changes: 1 addition & 2 deletions src/spec/cli.cr
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ module Spec
# :nodoc:
def self.add_location(file, line)
locations = @@locations ||= {} of String => Array(Int32)
lines = locations[File.expand_path(file)] ||= [] of Int32
lines << line
locations.put_if_absent(File.expand_path(file)) { [] of Int32 } << line
end

# :nodoc:
Expand Down
2 changes: 1 addition & 1 deletion src/spec/source.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module Spec
def self.read_line(file, line)
return nil unless File.file?(file)

lines = lines_cache[file] ||= File.read_lines(file)
lines = lines_cache.put_if_absent(file) { File.read_lines(file) }
lines[line - 1]?
end

Expand Down
9 changes: 4 additions & 5 deletions src/uri/params.cr
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ class URI
def self.parse(query : String) : self
parsed = {} of String => Array(String)
parse(query) do |key, value|
ary = parsed[key] ||= [] of String
ary.push value
parsed.put_if_absent(key) { [] of String } << value
end
Params.new(parsed)
end
Expand Down Expand Up @@ -297,9 +296,9 @@ class URI
# params.fetch_all("item") # => ["pencil", "book", "workbook", "keychain"]
# ```
def add(name, value)
raw_params[name] ||= [] of String
raw_params[name] = [] of String if raw_params[name] == [""]
raw_params[name] << value
params = raw_params.put_if_absent(name) { [] of String }
params.clear if params.size == 1 && params[0] == ""
params << value
end

# Sets all *values* for specified param *name* at once.
Expand Down

0 comments on commit c3bf074

Please sign in to comment.