Create an object/Native demonstration: Difference between revisions

From Rosetta Code
Content added Content deleted
(→‎{{header|Ruby}}: Add some important parts (lFencedHash#delete, FencedHash#store).)
(→‎{{header|Ruby}}: Add more tests. Implement FencedHash#clone, FencedHash#dup and FencedHash#freeze.)
Line 166: Line 166:
<lang ruby># fencedhash.rb
<lang ruby># fencedhash.rb
require 'forwardable'
require 'forwardable'

unless defined? KeyError
KeyError = IndexError
end


# A FencedHash acts like a Hash, but with a fence around its keys.
# A FencedHash acts like a Hash, but with a fence around its keys.
Line 191: Line 187:
:rassoc, :size, :to_a, :values, :values_at)
:rassoc, :size, :to_a, :values, :values_at)
attr_reader :default_proc
attr_reader :default_proc

# TODO: This causes test_each_rewind to fail!
def_delegators :@hash, :each


# Acts like Hash::[] but creates a FencedHash.
# Acts like Hash::[] but creates a FencedHash.
Line 209: Line 208:
raise ArgumentError, "wrong number of arguments"
raise ArgumentError, "wrong number of arguments"
end
end

super()


if block_given?
if block_given?
Line 225: Line 222:
args.first.each { |key| @hash[key] = @hash[key] }
args.first.each { |key| @hash[key] = @hash[key] }
end
end
end

def initialize_copy(orig)
super
@hash = @hash.dup
end
end


Line 230: Line 232:
# but resets the value to default.
# but resets the value to default.
def clear
def clear
@hash = @hash
@hash.each_key { |key| delete key }
@hash.each_key { |key| delete key }
self
self
Line 242: Line 245:
# .....
# .....
def default_proc=(proc_obj)
def default_proc=(proc_obj)
@hash = @hash

# Convert _proc_obj_ to a block parameter.
# Convert _proc_obj_ to a block parameter.
proc_obj = proc &proc_obj
proc_obj = proc &proc_obj
Line 253: Line 258:
# If _key_ is in the fence.....
# If _key_ is in the fence.....
def delete(key)
def delete(key)
@hash = @hash

begin
begin
original_value = @hash.fetch(key)
original_value = @hash.fetch(key)
rescue KeyError
rescue IndexError
# _key_ is not in the fence.
# _key_ is not in the fence.
if block_given?
if block_given?
Line 280: Line 287:
# not in the fence, then this method raises KeyError.
# not in the fence, then this method raises KeyError.
def store(key, value)
def store(key, value)
@hash = @hash
if @hash.has_key? key
if @hash.has_key? key
@hash.store(key, value)
@hash.store(key, value)
else
else
raise KeyError, "inserting new key: #{key}"
c = if defined? KeyError; then KeyError; else IndexError; end
raise c, "fence prevents new key: #{key}"
end
end
end
end
Line 294: Line 303:


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

def setup
def setup
@fh = FencedHash[:q => 11, :w => 22, :e => 33,
@fh = FencedHash[:q => 11, :w => 22, :e => 33,
Line 306: Line 323:
assert_equal 55, @fh[:t]
assert_equal 55, @fh[:t]
assert_equal 66, @fh[:y]
assert_equal 66, @fh[:y]
assert_nil @fh["q"]
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 4, @fh.length
@fh[:y] = "why?"
@fh[:t] = "tee!"
assert_equal 6, @fh.length
end

def test_each
count = 0
@fh.each do |key, value|
assert_kind_of Symbol, key
assert_kind_of Integer, value
assert_equal true, (@fh.has_key? key)
assert_equal true, (@fh.has_value? value)
count += 1
end
assert_equal 6, count
end
end


Line 316: Line 363:
assert_equal 55, @fh.fetch(:t)
assert_equal 55, @fh.fetch(:t)
assert_equal 66, @fh.fetch(:y)
assert_equal 66, @fh.fetch(:y)
assert_raises(KeyError) { @fh.fetch "q" }
assert_raises(KeyEx) { @fh.fetch :u }
end

def test_freeze
assert_equal false, @fh.frozen?
@fh.freeze

2.times do
assert_equal true, @fh.frozen?
assert_raises(FrozenEx) { @fh.clear }
assert_raises(FrozenEx) { @fh.delete :q }
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

def test_has_key
2.times do |t|
assert_equal true, (@fh.has_key? :y)
assert_equal true, (@fh.include? :y)
assert_equal true, (@fh.key? :y)
assert_equal true, (@fh.member? :y)

assert_equal false, (@fh.has_key? :u)
assert_equal false, (@fh.include? :u)
assert_equal false, (@fh.key? :u)
assert_equal false, (@fh.member? :u)

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

def test_inject
# To get an :inject method, FencedHash should mix in Enumerable.
assert_kind_of Enumerable, @fh
assert_equal 231, @fh.inject(0) { |sum, kv| sum + kv[1] }
end
end


def test_keys
def test_keys
assert_equal([:e, :q, :r, :t, :w, :y],
sort = proc { |a| a.sort_by { |o| o.to_s }}
assert_equal sort[[:q, :w, :e, :r, :t, :y]], sort[@fh.keys]
@fh.keys.sort_by { |o| o.to_s })
end
end


Line 327: Line 421:
assert_equal 6, @fh.length
assert_equal 6, @fh.length
assert_equal 6, @fh.size
assert_equal 6, @fh.size
end

def test_store
assert_raises(KeyEx) { @fh[:a] = 111 }
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 2, @fh.length
end
end
end


if RUBY_VERSION >= "1.9"
if RUBY_VERSION >= "1.9"
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
def test_insertion_order
assert_equal [:q, :w, :e, :r, :t, :y], @fh.keys
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

Revision as of 21:28, 16 February 2011

Create an object/Native demonstration is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

Task:

Create a Hash/Associative Array/Dictionary-like object, which is initialized with some default key/value pairs. The object should behave like a native Hash/Associative Array/Dictionary of the language, if any, but with the following differences:

  1. No new item can be added;
  2. Item cannot be deleted, (but native delete method may used to reset the item's value to default) ;

Objective:

The objective is not just create such object, but to demonstarion the language's native way of object creation. For some language, the task should show how the so-called Magic Methods work.

J

Given a list of keys and an associated list of values, the idiomatic way of expressing this concept in J would be:

<lang j>lookup=: values {~ keys&i.</lang>

For example:

<lang j> lookup=: 10 20 30 40 50 {~ (;:'this is a test')&i.

  lookup ;:'a test'

30 40</lang>

Notes:

1) While the result can not be modified or deleted, the name used to refer to it can be made to refer to something else, and once all references are lost it will be garbage collected.

2) In the above example, we have 5 values and 4 keys. The extra value is used when no key is found. If no extra value was provided, the "key not found" case would be an error case.

3) In J, objects are always referenced, but all data is passed by value. This means that objects can never be passed to a function -- only a reference to an object (its name) can be passed. This means that objects exist only in the way things are named, in J. So for the most part, we do not call things "objects" in J, and this task has nothing to do with what are called "objects" in J. However, this does demonstrate how things are created in J -- you write their definition, and can use them and/or assign to names or inspect them or whatever else.

JavaScript

This is a first demonstration of the task, but only implemented the functionality, not any native behavior, eg indexing. JavaScript experts may want to replace this one.

Works with: JavaScript version 1.7

<lang javascript>var keyError = new Error("Invalid Key Error (FixedKeyDict)") ;

function FixedKeyDict(obj) {

   var myDefault = new Object() ;
   var myData    = new Object() ;
   for(k in obj) {
       myDefault[k] = obj[k] ;
       myData[k]    = obj[k] ;
   }
   var gotKey = function(k) {
       for(kk in myDefault) {
           if(kk == k) return true ;
       }
       return false ;        
   } ;
   this.hasKey = gotKey ;
   var checkKey = function(k) {
       if(!gotKey(k))
           throw keyError ;
   } ;
  
   this.getItem = function(k) {
       checkKey(k) ;
       return myData[k];
   } ;
   
   this.setItem = function(k, v) {
       checkKey(k) ;
       myData[k] = v ;
   } ;
   
   this.resetItem = function(k) {
       checkKey(k) ;
       myData[k] = myDefault[k] ;      
   } ;
   
   this.delItem = this.resetItem ;
   
   this.clear   = function() {
       for(k in myDefault)
           myData[k] = myDefault[k] ;
   } ;
   
   this.iterator = function() {
       for(k in myDefault)
           yield (k);            
   } ;
   
   this.clone    = function() {
       return new FixedKeyDict(myDefault) ;
   }
   
   this.toStr = function() {
       var s = "" ;
       for(key in myData)
           s = s + key + " => " + myData[key] + ", " ;
       return "FixedKeyDict{" + s + "}" ;
   } ; 

}</lang>

Test run:

<lang javascript> const BR = "
\n"

var pl = function(s) {

   document.write(s + BR) ;

} ;

pl("

") ;

var o = { foo:101, bar:102 } ;

var h = new FixedKeyDict(o) ;
pl("Fixed Key Dict Created") ;
pl("toString   : " + h.toStr()) ;
pl("get an item: " + h.getItem("foo")) ;
pl("check a key: " + h.hasKey("boo")) ;
pl("ditto      : " + h.hasKey("bar")) ;
h.setItem("bar", 999) ;
pl("set an item: " + h.toStr()) ;
pl("Test iterator (or whatever)") ;
for(k in h.iterator())
    pl("  " + k + " => " + h.getItem(k)) ;
var g = h.clone() ;
pl("Clone a dict") ;
pl("  clone    : " + g.toStr()) ;
pl("  original : " + h.toStr()) ;
h.clear() ;
pl("clear or reset the dict") ;
pl("           : " + h.toStr()) ;
try {
    h.setItem("NoNewKey", 666 ) ;
} catch(e) {
    pl("error test : " + e.message) ;
}
</lang>

output :

<pre>
Fixed Key Dict Created
toString   : FixedKeyDict{foo => 101, bar => 102, }
get an item: 101
check a key: false
ditto      : true
set an item: FixedKeyDict{foo => 101, bar => 999, }
Test iterator (or whatever)
  foo => 101
  bar => 999
Clone a dict
  clone    : FixedKeyDict{foo => 101, bar => 102, }
  original : FixedKeyDict{foo => 101, bar => 999, }
clear or reset the dict
           : FixedKeyDict{foo => 101, bar => 102, }
error test : Invalid Key Error (FixedKeyDict)

Ruby

This example is under development. It was marked thus on 15/February/2011. Please help complete the example.

TODO: Add missing methods (FencedHash#delete_if). Write missing comments. Do more tests.

<lang ruby># fencedhash.rb require 'forwardable'

  1. A FencedHash acts like a Hash, but with a fence around its keys.
  2. After the creation of a FencedHash, one cannot add nor remove keys.
  3. Any attempt to insert a new key will raise KeyError. Any attempt to
  4. delete a key-value pair will keep the key but will reset the value to
  5. the default value.

class FencedHash < Object

 extend Forwardable
 include Enumerable
 #--
 # @hash: our Hash inside the fence
 # @default_proc: passes self, not @hash
 #++
 def_delegators(:@hash, :[], :assoc,
                :compare_by_identity, :compare_by_identity?,
                :default, :empty?, :fetch, :flatten,
                :has_key?, :has_value?, :hash, :include?,
                :key, :key?, :keys, :length, :member?,
                :rassoc, :size, :to_a, :values, :values_at)
 attr_reader :default_proc
 # TODO: This causes test_each_rewind to fail!
 def_delegators :@hash, :each
 # Acts like Hash::[] but creates a FencedHash.
 def self.[](*args)
   FencedHash.allocate.instance_eval do
     @hash = Hash[*args]
     self
   end
 end
 # call-seq:
 #   FencedHash.new(obj=nil [,keys])               -> fh
 #   FencedHash.new([keys]) { |fh, key| block }    -> fh
 #
 # Creates a FencedHash.....
 def initialize(*args, &block)
   if arguments.length > (block_given? && 1 || 2)
     raise ArgumentError, "wrong number of arguments"
   end
   if block_given?
     @default_proc = block
     @hash = Hash.new { |hash, key| yield self, key }
   else
     # FencedHash.new() acts like FencedHash.new(nil) because
     # if args.empty?, then args.shift returns nil.
     @hash = Hash.new(args.shift)
   end
   # For all keys in the fence, insert their default values.
   unless args.empty?
     args.first.each { |key| @hash[key] = @hash[key] }
   end
 end
 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.
 def clear
   @hash = @hash
   @hash.each_key { |key| delete key }
   self
 end
 # .....
 def default=(obj)
   @default_proc = nil
   @hash.default = obj
 end
 # .....
 def default_proc=(proc_obj)
   @hash = @hash
   # 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.....
 def delete(key)
   @hash = @hash
   begin
     original_value = @hash.fetch(key)
   rescue IndexError
     # _key_ is not in the fence.
     if block_given?
       yield key
     else
       @hash[key]
     end
   else
     # _key_ is in the fence. Must reset the value. From Ruby 1.9,
     # @hash remembers the insertion order of the keys. Must preserve
     # this insertion order and must not call @hash.delete(key).
     default = if @default_proc
                 @default_proc[self, key]
               else
                 @hash.default
               end
     @hash.store(key, default)
     original_value
   end
 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
     c = if defined? KeyError; then KeyError; else IndexError; end
     raise c, "fence prevents new key: #{key}"
   end
 end
 alias []= store

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 4, @fh.length
   @fh[:y] = "why?"
   @fh[:t] = "tee!"
   assert_equal 6, @fh.length
 end
 def test_each
   count = 0
   @fh.each do |key, value|
     assert_kind_of Symbol, key
     assert_kind_of Integer, value
     assert_equal true, (@fh.has_key? key)
     assert_equal true, (@fh.has_value? value)
     count += 1
   end
   assert_equal 6, count
 end
 def test_fetch
   assert_equal 11, @fh.fetch(:q)
   assert_equal 22, @fh.fetch(:w)
   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
 def test_freeze
   assert_equal false, @fh.frozen?
   @fh.freeze
   2.times do
     assert_equal true, @fh.frozen?
     assert_raises(FrozenEx) { @fh.clear }
     assert_raises(FrozenEx) { @fh.delete :q }
     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
 def test_has_key
   2.times do |t|
     assert_equal true, (@fh.has_key? :y)
     assert_equal true, (@fh.include? :y)
     assert_equal true, (@fh.key? :y)
     assert_equal true, (@fh.member? :y)
     assert_equal false, (@fh.has_key? :u)
     assert_equal false, (@fh.include? :u)
     assert_equal false, (@fh.key? :u)
     assert_equal false, (@fh.member? :u)
     # Repeat the tests.
     # The fence must prevent any changes to the keys.
     @fh.delete :y
     (@fh[:u] = "value") rescue "ok"
   end
 end
 def test_inject
   # To get an :inject method, FencedHash should mix in Enumerable.
   assert_kind_of Enumerable, @fh
   assert_equal 231, @fh.inject(0) { |sum, kv| sum + kv[1] }
 end
 def test_keys
   assert_equal([:e, :q, :r, :t, :w, :y],
                @fh.keys.sort_by { |o| o.to_s })
 end
 def test_length
   assert_equal 6, @fh.length
   assert_equal 6, @fh.size
 end
 def test_store
   assert_raises(KeyEx) { @fh[:a] = 111 }
   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 2, @fh.length
   end
 end
 if RUBY_VERSION >= "1.9"
   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</lang>