Create an object/Native demonstration: Difference between revisions

Content added Content deleted
(Added zkl)
(→‎{{header|Ruby}}: Replace my three-year-old attempt with a simpler version. Remove confusing default_proc feature. Throw out old tests. Forget Ruby older than 1.9.)
Line 514: Line 514:


=={{header|Ruby}}==
=={{header|Ruby}}==
<lang ruby># A FencedHash acts like a Hash, but with a fence around its keys.
{{in progress|lang=Ruby|day=15|month=February|year=2011}}
# One may change its values, but not its keys. Any attempt to insert
# a new key raises KeyError. One may delete a key, but this only
# restores its original value.
#
# FencedHash reimplements these Hash methods: #[] #[]= #clear #delete
# #delete_if #default #default= #each_key #each_pair #each_value
# #fetch #has_key? #keep_if #keys #length #values #values_at
class FencedHash


# call-seq:
TODO: Write comments for FencedHash::new, FencedHash#delete and related methods. Add more methods (merge, merge!, reject, reject!, select, select!, update). Explain why FencedHash#replace and FencedHash#shift will not exist.
# FencedHash.new(hash, obj=nil) -> fh
#
# Creates a FencedHash that takes its keys and original values from
# a source _hash_. The source _hash_ can be any object that
# responds to each_pair. Sets the default value for missing keys to
# _obj_, so FencedHash#[] returns _obj_ when a key is not in fence.
def initialize(hash, obj=nil)
@default = obj
@hash = {}
hash.each_pair do |key, value|
# @hash[key][0] = current value
# @hash[key][1] = original value
@hash[key] = [value, value]
end
end


def initialize_clone(orig)
<lang ruby># fencedhash.rb
# Object#clone calls here in Ruby 2.0. If _orig_ was frozen, then
require 'forwardable'
# each array of _values_ is frozen, so make frozen clones.
super
copy = {}
@hash.each_pair {|key, values| copy[key] = values.clone }
@hash = copy
end


def initialize_dup(orig)
# A FencedHash acts like a Hash, but with a fence around its keys.
# Object#dup calls here in Ruby 2.0. If _orig_ was frozen, then
# After the creation of a FencedHash, one cannot add nor remove keys.
# make duplicates that are not frozen.
# Any attempt to insert a new key will raise KeyError. Any attempt to
super
# delete a key-value pair will keep the key but will reset the value to
copy = {}
# the default value.
@hash.each_pair {|key, values| copy[key] = values.dup }
class FencedHash < Object
@hash = copy
extend Forwardable
end
include Enumerable


# Retrieves current value for _key_, like Hash#[]. If _key_ is not
#--
# in fence, returns default object.
# @hash: our Hash inside the fence
def [](key)
# @default_proc: passes self, not @hash
values = @hash[key]
#++
if values
def_delegators(:@hash, :[], :assoc,
values[0]
:compare_by_identity, :compare_by_identity?,
else
:default, :empty?, :fetch, :flatten,
@default
:has_key?, :has_value?, :hash, :include?,
:key, :key?, :keys, :length, :member?,
:rassoc, :size, :to_a,
:values, :values_at, :value?)
attr_reader :default_proc

# Acts like Hash::[] but creates a FencedHash.
def self.[](*args)
allocate.instance_eval do
@hash = Hash[*args]
self
end
end
end
end


# call-seq:
# call-seq:
# FencedHash.new(obj=nil [,keys]) -> fh
# fh[key] = value -> value
# FencedHash.new([keys]) { |fh, key| block } -> fh
# fh.store(key, value) -> value
#
#
# Sets _value_ for a _key_. Returns _value. If _key_ is not in
# Creates a FencedHash.....
# fence, raises KeyError.
def initialize(*args, &block)
def []=(key, value)
n = args.length
values = @hash[key]

if block_given?
if values
values[0] = value
raise ArgumentError, "wrong number of arguments" if n > 1

@default_proc = block
@hash = Hash.new { |hash, key| block[self, key] }
if n > 0
args[0].each { |key| @hash[key] = nil }
clear
end
else
else
raise ArgumentError, "wrong number of arguments" if n > 2
raise KeyError, "fence prevents adding new key: #{key.inspect}"

default = if n > 0 then n[0] else nil end
@hash = Hash.new(default)
if n > 1
args[1].each { |key| @hash[key] = default }
end
end
end
end
end
alias store []=


# Resets all keys to their original values. Returns self.
def initialize_copy(orig)
super
@hash = @hash.dup
end

# Clears all values. For each key-value pair, this retains the key
# but resets the value to default.
#--
# The line "@hash = @hash" checks that _self_ is not frozen, because
# Object#freeze only freezes _self_ and not @hash.
#++
def clear
def clear
@hash = @hash
@hash.each_value {|values| values[0] = values[1]}
@hash.each_key { |key| delete key }
self
self
end
end


# Resets _key_ to its original value. Returns old value before
# .....
# reset. If _key_ is not in fence, returns +nil+.
def default=(obj)
def delete(key)
@default_proc = nil
@hash.default = obj
values = @hash[key]
if values
old = values[0]
values[0] = values[1]
old # return old
end # else return nil
end
end


# .....
# call-seq:
# fh.delete_if {|key, value| block } -> fh
def default_proc=(proc_obj)
# fh.delete_if -> enumerator
# Convert _proc_obj_ to a block parameter.
proc_obj = proc &proc_obj

@hash.default_proc = proc { |hash, key| proc_obj[self, key] }
@default_proc = proc_obj
end

# Deletes the value of the key-value pair for _key_.
#
#
# If _key_ is in the fence.....
# Yields each _key_ with current _value_ to _block_. Resets _key_
# to its original value when block evaluates to true.
def delete(key)
def delete_if
@hash = @hash
if block_given?

@hash.each_pair do |key, values|
begin
yield(key, values[0]) and values[0] = values[1]
original_value = @hash.fetch(key)
rescue IndexError
# _key_ is not in the fence.
if block_given?
yield key
else
nil
end
end
self
else
else
enum_for(:delete_if) { @hash.size }
# _key_ is in the fence.
if @default_proc
@default_proc[self, key]
else
@hash[key] = @hash.default
end
original_value
end
end
end
end


# The default value for keys not in fence.
# .....
attr_accessor :default
def delete_if
return enum_for(:delete_if) unless block_given?


# call-seq:
@hash = @hash
@hash.each { |key, value| delete key if yield key, value }
# fh.each_key {|key| block} -> fh
# fh.each_key -> enumerator
self
end
#
# Yields each key in fence to the block.

def each_key(&block)
# Yields each key-value pair to the block, or returns an enumerator.
# Acts like Hash#each.
if block
@hash.each_key(&block)
def each &block # :yields: key, value
self
return enum_for(:each) unless block
@hash.each &block
end
alias each_pair each

# Yields each key to the block, or returns an enumerator.
# Acts like Hash#each_key.
def each_key &block # :yields: key
return enum_for(:each_key) unless block
@hash.each_key &block
end

# Yields each value to the block, or returns an enumerator.
# Acts like Hash#each_value.
def each_value &block # :yields: value
return enum_for(:each_value) unless block
@hash.each_value &block
end

# Returns true if _other_ is a FencedHash and has the same key-value
# pairs as _self_. Acts like Hash#eql?.
#--
# Consistent with FencedHash#hash because it delegates to @hash.hash.
#++
def eql?(other)
FencedHash === other and
@hash.eql?(other.instance_eval { @hash })
end

# Returns true if _other_ is a FencedHash and if the key-value pairs
# of _self_ equal those of _other_. Acts like Hash#==.
def ==(other)
FencedHash === other and
@hash == (other.instance_eval { @hash })
end

# .....
def keep_if
return enum_for(:keep_if) unless block_given?

@hash = @hash
@hash.each { |key, value| delete key unless yield key, value }
self
end

# Stores a _value_ for a _key_. This only works if _key_ is in the
# fence; FencedHash prevents the insertion of new keys. If _key_ is
# not in the fence, then this method raises KeyError.
def store(key, value)
@hash = @hash
if @hash.has_key? key
@hash.store(key, value)
else
else
enum_for(:each_key) { @hash.size }
c = if defined? KeyError then KeyError else IndexError end
raise c, "fence prevents new key: #{key}"
end
end
end
end
alias []= store


# call-seq:
# Converts _self_ to a regular Hash. Returns a new Hash that has the
# same key-value pairs as _self_.
# fh.each_pair {|key, value| block} -> fh
# fh.each_pair -> enumerator
def to_hash
#
@hash.dup
# Yields each key-value pair to the block, like Hash#each_pair.
end
# This yields each [key, value] as an array of 2 elements.

def each_pair
# Converts _self_ to a String.
if block_given?
def to_s
@hash.each_pair {|key, values| yield [key, values[0]] }
"#<#{self.class}: #{@hash.inspect}>"
self
end
else
alias inspect to_s
enum_for(:each_pair) { @hash.size }
end</lang>

<lang ruby># fh-test.rb
require 'fencedhash'
require 'test/unit'

class TestFencedHash < Test::Unit::TestCase
if RUBY_VERSION >= "1.9"
KeyEx = KeyError
FrozenEx = RuntimeError
else
KeyEx = IndexError
FrozenEx = TypeError
end

def setup
@fh = FencedHash[:q => 11, :w => 22, :e => 33,
:r => 44, :t => 55, :y => 66]
end

def test_bracket_operator
assert_equal 11, @fh[:q]
assert_equal 22, @fh[:w]
assert_equal 33, @fh[:e]
assert_equal 44, @fh[:r]
assert_equal 55, @fh[:t]
assert_equal 66, @fh[:y]
assert_nil @fh[:u]
end

def test_delete
assert_equal 44, (@fh.delete :r)
assert_nil @fh.fetch(:r)
assert_nil @fh.delete(:r)
assert_nil @fh.delete(:u)
@fh[:r] = "replacement"
assert_equal "replacement", (@fh.delete :r)
end

def test_delete_if
a = @fh.delete_if { |key, value| key == :t || value == 66 }
assert_same @fh, a
assert_equal 2, @fh.values.grep(nil).length
@fh[:y] = "why?"
@fh[:t] = "tea!"
assert_equal 0, @fh.values.grep(nil).length
end

def test_default
fruit = FencedHash.new(0, [:apple, :banana, :cranberry])
assert_equal [0, 0, 0], fruit.values
fruit[:apple] += 1
fruit[:banana] += 5
fruit[:cranberry] *= 5
assert_equal 1, fruit[:apple]
assert_equal 5, fruit[:banana]
assert_equal 0, fruit[:cranberry]
assert_equal 0, fruit.default
end

def test_default_assign
assert_nil @fh.default
@fh.delete :w

@fh.default = -1
assert_equal -1, @fh.default
@fh.delete :e

assert_nil @fh[:w]
assert_equal -1, @fh[:e]
end

def test_default_proc
count = 0
fruit = FencedHash.new([:apple, :banana, :cranberry]) do |h, k|
if h.key? k then h[k] = [] else count += 1 end
end
end
fruit[:apple].push :red
fruit[:banana].concat [:green, :yellow]
fruit[:cranberry].push :red
assert_equal 1, fruit[:orange]
assert_equal [:red], fruit[:apple]
assert_equal [:green, :yellow], fruit[:banana]
assert_equal [:red], fruit.delete(:cranberry)
assert_equal 2, fruit[:orange]
assert_equal [], fruit[:cranberry]
assert_nil fruit.delete(:orange)
assert_equal 3, fruit[:orange]
assert_equal [], fruit.default_proc[FencedHash[1 => 2], 1]
end
end


# call-seq
def test_each
# fh.each_value {|value| block} -> fh
count = 0
# fh.each_value -> enumerator
@fh.each do |key, value|
#
assert_kind_of Symbol, key
# Yields current value of each key-value pair to the block.
assert_kind_of Integer, value
def each_value
assert_equal true, (@fh.has_key? key)
if block_given?
assert_equal true, (@fh.has_value? value)
@hash.each_value {|values| yield values[0] }
count += 1
else
enum_for(:each_value) { @hash.size }
end
end
assert_equal 6, count
end
end


# call-seq:
def test_eql?
# fenhsh.fetch(key [,default])
other = FencedHash[:r, 44, :t, 55, :y, 66,
# fenhsh.fetch(key) {|key| block }
:q, 11, :w, 22, :e, 33]
#
float = FencedHash[:y, 66.0, :t, 55.0, :r, 44.0,
# Fetches value for _key_. Takes same arguments as Hash#fetch.
:e, 33.0, :w, 22.0, :q, 11.0]
def fetch(*argv)
tt = [true, true]
ff = [false, false]
argc = argv.length
unless argc.between?(1, 2)

raise(ArgumentError,
if RUBY_VERSION >= "1.9"
"wrong number of arguments (#{argc} for 1..2)")
assert_equal tt, [(@fh.eql? other), (other.eql? @fh)]
end
assert_equal ff, [(@fh.eql? float), (float.eql? @fh)]
if argc == 2 and block_given?
assert_equal ff, [(other.eql? float), (float.eql? other)]
warn("#{caller[0]}: warning: " +
"block supersedes default value argument")
end
end


key, default = argv
assert_equal tt, [@fh == other, other == @fh]
values = @hash[key]
assert_equal tt, [@fh == float, float == @fh]
if values
assert_equal tt, [other == float, float == other]
values[0]

h = @fh.to_hash
elsif block_given?
if RUBY_VERSION >= "1.9"
yield key
elsif argc == 2
assert_equal ff, [(@fh.eql? h), (h.eql? @fh)]
default
else
raise KeyError, "key not found: #{key.inspect}"
end
end
assert_equal ff, [@fh == h, h == @fh]
end
end


# Freezes this FencedHash.
def test_fetch
def freeze
assert_equal 11, @fh.fetch(:q)
@hash.each_value {|values| values.freeze }
assert_equal 22, @fh.fetch(:w)
super
assert_equal 33, @fh.fetch(:e)
assert_equal 44, @fh.fetch(:r)
assert_equal 55, @fh.fetch(:t)
assert_equal 66, @fh.fetch(:y)
assert_raises(KeyEx) { @fh.fetch :u }
end
end


# Returns true if _key_ is in fence.
def test_freeze
def has_key?(key)
assert_equal false, @fh.frozen?
@fh.freeze
@hash.has_key?(key)

2.times do
assert_equal true, @fh.frozen?
assert_raises(FrozenEx) { @fh.clear }
assert_raises(FrozenEx) { @fh.delete :q }
assert_raises(FrozenEx) { @fh.delete_if { true } }
assert_raises(FrozenEx) { @fh.keep_if { false } }
assert_raises(FrozenEx) { @fh.store :w, "different" }
assert_raises(FrozenEx) { @fh[:w] = "different" }

# Repeat the tests with a clone. The clone must be frozen.
@fh = @fh.clone
end

# A duplicate is not frozen.
@fh = @fh.dup
assert_equal false, @fh.frozen?
@fh[:w] = "different"
assert_equal "different", @fh[:w]
end
end
alias include? has_key?
alias member? has_key?


# call-seq:
def test_has_key
# fh.keep_if {|key, value| block } -> fh
2.times do |t|
# fh.keep_if -> enumerator
assert_equal true, (@fh.has_key? :y)
#
assert_equal true, (@fh.include? :y)
# Yields each _key_ with current _value_ to _block_. Resets _key_
assert_equal true, (@fh.key? :y)
# to its original value when block evaluates to false.
assert_equal true, (@fh.member? :y)
def keep_if

if block_given?
assert_equal false, (@fh.has_key? :u)
@hash.each_pair do |key, values|
assert_equal false, (@fh.include? :u)
yield(key, values[0]) or values[0] = values[1]
assert_equal false, (@fh.key? :u)
end
assert_equal false, (@fh.member? :u)
self

else
# Repeat the tests.
enum_for(:keep_if) { @hash.size }
# The fence must prevent any changes to the keys.
@fh.delete :y
(@fh[:u] = "value") rescue "ok"
end
end
end
end


# Returns array of keys in fence.
def test_has_value
def keys
assert_equal true, (@fh.has_value? 22)
@hash.keys
assert_equal true, (@fh.value? 22)

assert_equal false, (@fh.has_value? 4444)
assert_equal false, (@fh.value? 4444)
end
end


# Returns number of key-value pairs.
def test_inject
def length
# To get an :inject method, FencedHash should mix in Enumerable.
@hash.length
assert_kind_of Enumerable, @fh
assert_equal 231, @fh.inject(0) { |sum, kv| sum + kv[1] }
end
end
alias size length


# Converts self to a regular Hash.
def test_keep_if
def to_h
a = @fh.keep_if { |key, value| key == :t || value == 66 }
result = Hash.new(@default)
assert_same @fh, a
assert_equal 4, @fh.values.grep(nil).length
@hash.each_pair {|key, values| result[key] = values[0]}
@fh.delete :y
result
@fh.delete :t
assert_equal 6, @fh.values.grep(nil).length
end
end


# Converts self to a String.
def test_keys
def to_s
assert_equal([:e, :q, :r, :t, :w, :y],
"#<#{self.class}: #{to_h}>"
@fh.keys.sort_by { |o| o.to_s })
end
end
alias inspect to_s


# Returns array of current values.
def test_length
def values
assert_equal 6, @fh.length
@hash.each_value.map {|values| values[0]}
assert_equal 6, @fh.size
end
end


# Returns array of current values for keys, like Hash#values_at.
def test_store
def values_at(*keys)
assert_raises(KeyEx) { @fh[:a] = 111 }
keys.map {|key| self[key]}
assert_equal 222, (@fh[:e] = 222)
assert_equal 222, (@fh.fetch :e)
assert_equal 333, @fh.store(:e, 333)
assert_equal 333, @fh[:e]
end

def test_values
assert_equal [11, 22, 33, 44, 55, 66], @fh.values.sort!
end

if RUBY_VERSION >= "1.8.7"
def test_delete_if_enum
a = @fh.delete_if.with_index { |kv, i| i >= 2 }
assert_same @fh, a
assert_equal 4, @fh.values.grep(nil).length
end

def test_keep_if_enum
a = @fh.keep_if.with_index { |kv, i| i >= 2 }
assert_same @fh, a
assert_equal 2, @fh.values.grep(nil).length
end
end

if RUBY_VERSION >= "1.9"
def test_class_bracket_operator
from_pairs = FencedHash[10, "ten", 20, "twenty", 30, "thirty"]
from_alist = FencedHash[ [ [10, "ten"], [20, "twenty"], [30, "thirty"] ] ]
from_hash = FencedHash[10 => "ten", 20 => "twenty", 30 => "thirty"]
from_fhash = FencedHash[from_pairs]

[from_pairs, from_alist, from_hash, from_fhash, from_pairs
].each_cons(2) do |a, b|
assert_equal a, b
assert_not_same a, b
end
end

def test_default_proc_assign
assert_nil @fh.default_proc
p = @fh.default_proc = proc { |h, k| h[k] = :deleted }
assert_same p, @fh.default_proc

assert_equal 11, @fh.delete(:q)
assert_equal :deleted, @fh[:q]
assert_raises(KeyEx) { @fh[:u] }

@fh.default = :value
assert_nil @fh.default_proc
@fh.default_proc = p
assert_nil @fh.default
end

def test_each_rewind
class << @fh
attr_reader :test_rewind
def rewind
@test_rewind = "correct"
end
end
assert_nil @fh.test_rewind

# @fh.each.rewind must call @fh.rewind. If @fh forwards :each
# to another object then this test fails.
@fh.each.rewind
assert_equal "correct", @fh.test_rewind
end

def test_insertion_order
assert_equal [:q, :w, :e, :r, :t, :y], @fh.keys
assert_equal [11, 22, 33, 44, 55, 66], @fh.values
end

def test_key
assert_equal :q, @fh.key(11)
assert_equal :w, @fh.key(22)
assert_equal :e, @fh.key(33)
assert_equal :r, @fh.key(44)
assert_equal :t, @fh.key(55)
assert_equal :y, @fh.key(66)
assert_nil @fh.key(77)
end
end
end
end</lang>
end</lang>