Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,222 @@
module Hashery
# TODO: Should associations be singleton?
#
# TODO: Is it really wise to keep a table of all associations?
# Association is a general binary association that allows one
# object to be associated with another. It has a variety of uses,
# such as linked-lists, simple ordered maps and mixed collections,
# among them.
#
# NOTE: This class is still fairly experimental. And it is not
# loaded along with the other Hashery libraries when using
# `require 'hashery'`. It must be required independently.
#
# Associations can be used to draw simple relationships.
#
# :Apple >> :Fruit
# :Apple >> :Red
#
# :Apple.associations #=> [ :Fruit, :Red ]
#
# It can also be used for simple lists of ordered pairs.
#
# c = [ :a >> 1, :b >> 2 ]
# c.each { |k,v| puts "#{k} associated with #{v} }
#
# produces
#
# a associated with 1
# b associated with 2
#
# The method :>> is used to construct the association.
# It is a rarely used method so it is generally available.
# But you can't use it for any of the following classes
# becuase they use #>> for other things.
#
# Bignum
# Fixnum
# Date
# IPAddr
# Process::Status
#
class Association
include Comparable
class << self
#
# Store association references.
#
# Returns `Hash` of all associaitons.
#
def reference
@reference ||= Hash.new{ |h,k,v| h[k]=[] }
end
#
# Shortcut for #new.
#
# index - The "index key" of the association.
# value - The "value" of the association.
#
# Returns `Association`.
#
def [](index, value)
new(index, value)
end
#def new(index, value)
# lookup[[index, value]] ||= new(index, value)
#end
#def lookup
# @lookup ||= {}
#end
end
#
# The "index key" of the association.
#
attr_accessor :index
#
# The "value" of the association.
#
attr_accessor :value
#
# Initialize new Association.
#
# index - The "index key" of the association.
# value - The "value" of the association.
#
def initialize(index, value=nil)
@index = index
@value = value
unless index.associations.include?(value)
index.associations << value
end
end
#
# Compare the values of two associations.
#
# TODO: Comparions with non-associations?
#
# assoc - The other `Association`.
#
# Returns [Integer] `1`, `0`, or `-1`.
#
def <=>(assoc)
return -1 if self.value < assoc.value
return 1 if self.value > assoc.value
return 0 if self.value == assoc.value
end
#
# Invert association, making the index the value and vice-versa.
#
# Returns [Array] with two-elements reversed.
#
def invert!
temp = @index
@index = @value
@value = temp
end
#
# Produce a string representation.
#
# Returns [String].
#
def to_s
return "#{index} >> #{value}"
end
#
# Produce a literal code string for creating an association.
#
# Returns [String].
#
def inspect
"#{index.inspect} >> #{value.inspect}"
end
#
# Convert to two-element associative array.
#
# Returns [Array] Two-element Array of index and value pair.
#
def to_ary
[index, value]
end
#
# Object extensions.
#
module Kernel
#
# Define an association for +self+.
#
# to - The value of the association.
#
# Returns [Association].
#
def >>(to)
Association.new(self, to)
end
#
# List of associations for this object.
#
# Returns an `Array` of `Associations`.
#
def associations
Association.reference[self]
end
end
end
end
class Object #:nodoc:
include Hashery::Association::Kernel
end
#--
# Setup the >> method in classes that use it already.
#
# This is a bad idea b/c it can cause backward compability issues.
#
# class Bignum
# alias_method( :rshift, :>>) if method_defined?(:>>)
# remove_method :>>
# end
#
# class Fixnum
# alias_method( :rshift, :>>) if method_defined?(:>>)
# remove_method :>>
# end
#
# class Date
# alias_method( :months_later, :>>) if method_defined?(:>>)
# remove_method :>>
# end
#
# class IPAddr
# alias_method( :rshift, :>>) if method_defined?(:>>)
# remove_method :>>
# end
#
# class Process::Status
# alias_method( :rshift, :>>) if method_defined?(:>>)
# remove_method :>>
# end
#++
# Copyright (c) 2005 Rubyworks, Thomas Sawyer
@@ -0,0 +1,164 @@
require 'hashery/crud_hash'
module Hashery
# CastingHash is just like CRUDHash, except that both keys and values
# can be passed through casting procedures.
#
class CastingHash < CRUDHash
#
# Like `#new` but can take a priming Hash or Array-pairs.
#
# hash - Hash-like object.
#
# Examples
#
# CastingHash[:a,1,:b,2]
#
# Returns `CastingHash`.
#
def self.[](hash)
s = new
hash.each{ |k,v| s[k] = v }
s
end
#
# Unlike traditional Hash a CastingHash's block argument
# coerces key/value pairs when #store is called.
#
# default - Default value.
# cast_proc - Casting procedure.
#
def initialize(default=nil, &cast_proc)
@cast_proc = cast_proc
super(default, &nil)
end
#
# The cast procedure.
#
# proc - Casting procedure.
#
# Returns `Proc` used for casting.
#
def cast_proc(&proc)
@cast_proc = proc if proc
@cast_proc
end
#
# Set `cast_proc`. This procedure must take two arguments (`key, value`)
# and return the same.
#
# proc - Casting procedure.
#
# Returns +proc+.
#
def cast_proc=(proc)
raise ArgumentError unless Proc === proc or NilClass === proc
@cast_proc = proc
end
#
# CRUD method for create and update. Unlike the parent class
# the key, value pair are passed threw the cast_proc before
# being set in the underlying hash table.
#
# key - Key of entry.
# value - Value of entry.
#
# Returns the +value+.
#
def store(key, value)
super(*cast_pair(key, value))
end
#
# Replace current entries with those from another Hash,
# or Hash-like object. Each entry is run through the
# casting procedure as it is added.
#
# other - Hash-like object.
#
# Returns +self+.
#
def replace(other)
super cast(other)
end
#
# Convert the CastingHash to a regular Hash.
#
# Returns an ordinary `Hash`.
#
def to_hash
h = {}; each{ |k,v| h[k] = v }; h
end
#
# Returns an ordinary `Hash`.
#
alias_method :to_h, :to_hash
#
# Recast all entries via the cast procedure.
#
# TODO: Isn't this the same as `#rehash`?
#
# Returns +self+.
#
def recast!
replace self
end
private
#
# If `cast_proc` is defined then use it to process key-value pair,
# otherwise return them as is.
#
# key - Key of entry.
# value - Value of entry.
#
# Returns `Array` of key-value pair.
#
def cast_pair(key, value)
if cast_proc
return cast_proc.call(key, value)
else
return key, value
end
end
#
# Cast a given +hash+ according to the `#key_proc` and `#value_proc`.
#
# hash - A `Hash` or anything the responds to `#each` like a hash.
#
# Returns a recasted `Hash`.
#
def cast(hash)
h = {}
hash.each do |k,v|
k, v = cast_pair(k, v)
h[k] = v
end
h
end
end
end
# TODO: Should we add #to_casting_hash to Hash classs?
#class Hash
#
# # Convert a Hash to a CastingHash.
# def to_casting_hash(value_cast=nil, &key_cast)
# CastingHash.new(self, value_cast, &key_cast)
# end
#
#end
@@ -0,0 +1,119 @@
class Hash
#
# Create a hash given an `initial_hash`.
#
# initial_hash - Hash or hash-like object to use as priming data.
# block - Procedure used by initialize (e.g. default_proc).
#
# Returns a `Hash`.
#
def self.create(initial_hash={}, &block)
o = new &block
o.update(initial_hash)
o
end
#
# Like #fetch but returns the results of calling `default_proc`, if defined,
# otherwise `default`.
#
# key - Hash key to lookup.
#
# Returns value of Hash entry or `nil`.
#
def retrieve(key)
fetch(key, default_proc ? default_proc[self, key] : default)
end
#
# Convert to Hash.
#
def to_hash
dup # -or- `h = {}; each{ |k,v| h[k] = v }; h` ?
end \
unless method_defined?(:to_hash)
#
# For a Hash, `#to_h` is the same as `#to_hash`.
#
alias :to_h :to_hash \
unless method_defined?(:to_h)
#
# Synonym for Hash#rekey, but modifies the receiver in place (and returns it).
#
# key_map - Hash of old key to new key.
# block - Procedure to convert keys, which can take just the key
# or both key and value as arguments.
#
# Examples
#
# foo = { :name=>'Gavin', :wife=>:Lisa }
# foo.rekey!{ |k| k.to_s } #=> { "name"=>"Gavin", "wife"=>:Lisa }
# foo.inspect #=> { "name"=>"Gavin", "wife"=>:Lisa }
#
# Returns `Hash`.
#
def rekey(key_map=nil, &block)
if !(key_map or block)
block = lambda{|k| k.to_sym}
end
key_map ||= {}
hash = {}
(keys - key_map.keys).each do |key|
hash[key] = self[key]
end
key_map.each do |from, to|
hash[to] = self[from] if key?(from)
end
hash2 = {}
if block
case block.arity
when 0
raise ArgumentError, "arity of 0 for #{block.inspect}"
when 2
hash.each do |k,v|
nk = block.call(k,v)
hash2[nk] = v
end
else
hash.each do |k,v|
nk = block[k]
hash2[nk] = v
end
end
else
hash2 = hash
end
hash2
end
#
# Synonym for Hash#rekey, but modifies the receiver in place (and returns it).
#
# key_map - Hash of old key to new key.
# block - Procedure to convert keys, which can take just the key
# or both key and value as arguments.
#
# Examples
#
# foo = { :name=>'Gavin', :wife=>:Lisa }
# foo.rekey!{ |k| k.to_s } #=> { "name"=>"Gavin", "wife"=>:Lisa }
# foo #=> { "name"=>"Gavin", "wife"=>:Lisa }
#
# Returns `Hash`.
#
def rekey!(key_map=nil, &block)
replace(rekey(key_map, &block))
end
end
@@ -0,0 +1,405 @@
require 'hashery/core_ext'
module Hashery
# The CRUDHash is essentailly the same as the Hash class, but it reduces the
# the set of necessary methods to the fundametal CRUD requirements. All other
# methods route through these CRUD methods. This is a better general design,
# although it is, of course, a little bit slower. The utility of this class
# becomes appearent when subclassing or delegating, as only a handful of methods
# need to be changed for all other methods to work accordingly.
#
# In addition to the CRUD features, CRUDHash supports a `#key_proc`, akin to
# `#default_proc`, that can be used to normalize keys.
#
# The CRUD methods are:
#
# * key?
# * fetch
# * store
# * delete
#
# In addition to these main methods, there are these supporting "CRUD" methods:
#
# * default
# * default_proc
# * default_proc=
# * key_proc
# * key_proc=
#
class CRUDHash < ::Hash
#
# Dummy object for null arguments.
#
NA = Object.new
#
# This method is overridden to ensure that new entries pass through
# the `#store` method.
#
# hash - [#each] Single Hash, associative array or just a list of pairs.
#
def self.[](*hash)
h = new
if hash.size == 1
hash.first.each do |k,v|
h.store(k, v)
end
else
hash.each do |(k,v)|
h.store(k, v)
end
end
h
end
#
# Alternate to #new which auto-creates sub-dictionaries as needed.
# By default the `default_proc` procuced a empty Hash and is
# self-referential so every such Hash also has the same `default_proc`.
#
# args - Pass-thru arguments to `#new`.
# block - Alternate internal procedure for default proc.
#
# Examples
#
# d = CRUDHash.auto
# d["a"]["b"]["c"] = "abc" #=> { "a"=>{"b"=>{"c"=>"abc"}}}
#
# Returns `Hash`.
#
def self.auto(*args, &block)
if block
leet = lambda { |hsh, key| hsh[key] = block.call(hsh, key) }
else
leet = lambda { |hsh, key| hsh[key] = new(&leet) }
end
new(*args, &leet)
end
#
# Set `key_proc`.
#
# Examples
#
# ch = CRUDHash.new
# ch.key_proc = Proc.new{ |key| key.to_sym }
#
# Returns `Proc`.
#
def key_proc=(proc)
raise ArgumentError unless Proc === proc or NilClass === proc
@key_proc = proc
end
#
# Get/set `key_proc`.
#
# Examples
#
# ch = CRUDHash.new
# ch.key_proc
#
# Returns `Proc`.
#
def key_proc(&block)
@key_proc = block if block
@key_proc
end
#
# Allow `#default_proc` to take a block.
#
# block - The `Proc` object to set the `default_proc`.
#
# Returns `Proc`, the `default_proc`.
#
def default_proc(&block)
self.default_proc = block if block
super()
end
#
# CRUD method for checking if key exists.
#
# key - Hash key to lookup.
#
# Returns `true/false`.
#
def key?(key)
super cast_key(key)
end
#
# CRUD method for read. This method gets the value for a given key.
# An error is raised if the key is not present, but an optional argument
# can be provided to be returned instead.
#
# key - Hash key to lookup.
# default - Value to return if key is not present.
#
# Raises KeyError when key is not found and default has not been given.
#
# Returns the `Object` that is the Hash entry's value.
#
def fetch(key, *default)
super(cast_key(key), *default)
end
#
# CRUD method for create and update.
#
# key - The `Object` to act as indexing key.
# value - The `Object` to associate with key.
#
# Returns +value+.
#
def store(key, value)
super(cast_key(key), value)
end
#
# CRUD method for delete.
#
# key - Hash key to remove.
#
# Returns value of deleted Hash entry.
#
def delete(key)
super cast_key(key)
end
# END OF CRUD METHODS
#
# Like #fetch but returns the results of calling `default_proc`, if defined,
# otherwise `default`.
#
# key - Hash key to lookup.
#
# Returns value of Hash entry or `nil`.
#
def retrieve(key)
if key?(key)
fetch(key)
else
default_proc ? default_proc.call(self, key) : default
end
end
#
# Method for reading value. Returns `nil` if key is not present.
#
# Note that this method used to be the CRUD method instead of #retrieve. Complaints about
# #read being indicative of an IO object (though in my opinion that is a bad asumption) have
# led to this method's deprecation.
#
# key - Hash key to lookup.
#
# Returns value of Hash entry.
#
def read(key)
warn "The #read method as been deprecated. Use #retrieve instead."
retrieve(key)
end
#
# Update Hash with +assoc+.
#
# assoc - Two-element `Array` or a `Hash`.
#
# Returns +assoc+.
#
def <<(assoc)
case assoc
when Hash
update(assoc)
when Array
assoc.each_slice(2) do |(k,v)|
store(k,v)
end
else
raise ArgumentError # or TypeError ?
end
end
#
# Operator for `#retrieve`.
#
# key - Index key to lookup.
#
# Returns `Object` value of key.
#
def [](key)
retrieve(key)
end
#
# Operator for `#store`.
#
# key - The `Object` to act as indexing key.
# value - The `Object` to associate with key.
#
# Returns +value+.
#
def []=(key,value)
store(key,value)
end
#
# Update the Hash with another hash.
#
# other - Other hash or hash-like object to add to the hash.
#
# Returns +self+.
#
def update(other)
other.each do |k,v|
store(k, v)
end
self
end
#
# Alias for `#update`.
#
alias merge! update
#
# Merge the Hash with another hash, returning a new Hash.
#
# other - Other hash or hash-like object to add to the hash.
#
# Returns `Hash`.
#
def merge(other)
#super(other.rekey{ |key| cast_key(key) })
copy = dup
other.each{ |k,v| copy.store(k, v) }
copy
end
#
# Iterate over each hash pair.
#
def each #:yield:
if block_given?
keys.each do |k|
yield(k, retrieve(k))
end
else
to_enum(:each)
end
end
#
# Alias for #each.
#
alias each_pair each
#
# Alias for `#key?`.
#
alias has_key? key?
#
# Alias for `#key?`.
#
alias member? key?
#
# Alias for `#key?`.
#
alias include? key? # why isn't it an alias for `#has_value?` ?
#
# Replace current entries with those from another Hash,
# or Hash-like object. Each entry is run through the
# casting procedure as it is added.
#
# other - Hash-like object.
#
# Returns +self+.
#
def replace(other)
super cast(other)
end
#
# Get the values at.
#
# keys - List of keys to lookup.
#
# Returns `Array` of values.
#
def values_at(*keys)
super *keys.map{ |key| cast_key(key) }
end
# Convert CRUDHash to regular Hash.
#
# TODO: Since a CRUDHash is a subclass of Hash should #to_hash just `#dup`
# insted of converting to traditional Hash?
#
def to_hash
h = {}; each{ |k,v| h[k] = v }; h
end #unless method_defined?(:to_hash)
#
# Convert CRUDHash to regular Hash.
#
# TODO: Since a CRUDHash is a subclass of Hash should #to_h just `#dup`
# insted of converting to traditional Hash?
#
# Returns `Hash`.
#
alias :to_h :to_hash
private
#
# Cast a given `hash` in accordance to the `#key_proc`.
#
# hash - Any object the responds to `#each` like a Hash.
#
# Returns `Hash`.
#
def cast(hash)
h = {}
hash.each do |k,v|
h[cast_key(k)] = v
end
h
end
#
# Callback for normalizing hash keys.
#
# key - Index key.
#
# Returns key after passing through the `key_proc`.
#
def cast_key(key)
@key_proc ? @key_proc.call(key) : key
end
# TODO: Consider value callback procs for future version of CRUDHash.
#
# #
# # Callback for writing value.
# #
# def cast_write(value)
# @write_proc ? @write_proc.call(value) : value
# end
#
# #
# # Callback for reading value.
# #
# def cast_read(value)
# @read_proc ? @read_proc.call(value) : value
# end
end
end
@@ -0,0 +1,641 @@
module Hashery
# The Dictionary class is a Hash that preserves order.
# So it has some array-like extensions also. By defualt
# a Dictionary object preserves insertion order, but any
# order can be specified including alphabetical key order.
#
# Using a Dictionary is almost the same as using a Hash.
#
# # You can do simply
# hsh = Dictionary.new
# hsh['z'] = 1
# hsh['a'] = 2
# hsh['c'] = 3
# p hsh.keys #=> ['z','a','c']
#
# # or using Dictionary[] method
# hsh = Dictionary['z', 1, 'a', 2, 'c', 3]
# p hsh.keys #=> ['z','a','c']
#
# # but this don't preserve order
# hsh = Dictionary['z'=>1, 'a'=>2, 'c'=>3]
# p hsh.keys #=> ['a','c','z']
#
# # Dictionary has useful extensions: push, pop and unshift
# p hsh.push('to_end', 15) #=> true, key added
# p hsh.push('to_end', 30) #=> false, already - nothing happen
# p hsh.unshift('to_begin', 50) #=> true, key added
# p hsh.unshift('to_begin', 60) #=> false, already - nothing happen
# p hsh.keys #=> ["to_begin", "a", "c", "z", "to_end"]
# p hsh.pop #=> ["to_end", 15], if nothing remains, return nil
# p hsh.keys #=> ["to_begin", "a", "c", "z"]
# p hsh.shift #=> ["to_begin", 30], if nothing remains, return nil
#
# == Notes
#
# * You can use #order_by to set internal sort order.
# * #<< takes a two element [k,v] array and inserts.
# * Use ::auto which creates Dictionay sub-entries as needed.
# * And ::alpha which creates a new Dictionary sorted by key.
#
# == Acknowledgments
#
# Dictionary is a port of OrderHash 2.0 Copyright (c) 2005 Jan Molic.
#
# People who have contributed to this class since then include:
#
# * Andrew Johnson (merge, to_a, inspect, shift and Hash[])
# * Jeff Sharpe (reverse and reverse!)
# * Thomas Leitner (has_key? and key?)
#
# OrderedHash is public domain.
#
class Dictionary
include Enumerable
class << self
#
# Create a new Dictionary storing argument pairs as an initial mapping.
#
# TODO: Is this needed? Doesn't the super class do this?
#
# Returns Dictionary instance.
#
def [](*args)
hsh = new
if Hash === args[0]
hsh.replace(args[0])
elsif (args.size % 2) != 0
raise ArgumentError, "odd number of elements for Hash"
else
while !args.empty?
hsh[args.shift] = args.shift
end
end
hsh
end
#
# Like #new but the block sets the order instead of the default.
#
# Dictionary.new_by{ |k,v| k }
#
def new_by(*args, &blk)
new(*args).order_by(&blk)
end
#
# Alternate to #new which creates a dictionary sorted by the key as a string.
#
# d = Dictionary.alphabetic
# d["z"] = 1
# d["y"] = 2
# d["x"] = 3
# d #=> {"x"=>3,"y"=>2,"z"=>2}
#
# This is equivalent to:
#
# Dictionary.new.order_by { |key,value| key.to_s }
#
def alphabetic(*args, &block)
new(*args, &block).order_by { |key,value| key.to_s }
end
# DEPRECATED: Use #alphabetic instead.
alias :alpha :alphabetic
#
# Alternate to #new which auto-creates sub-dictionaries as needed.
#
# Examples
#
# d = Dictionary.auto
# d["a"]["b"]["c"] = "abc" #=> { "a"=>{"b"=>{"c"=>"abc"}}}
#
def auto(*args)
#AutoDictionary.new(*args)
leet = lambda { |hsh, key| hsh[key] = new(&leet) }
new(*args, &leet)
end
end
#
# New Dictiionary.
#
def initialize(*args, &blk)
@order = []
@order_by = nil
if blk
dict = self # This ensures automatic key entry effect the
oblk = lambda{ |hsh, key| blk[dict,key] } # dictionary rather then just the interal hash.
@hash = Hash.new(*args, &oblk)
else
@hash = Hash.new(*args)
end
end
#
# Order of keys.
#
# Returns [Array].
#
def order
reorder if @order_by
@order
end
#
# Keep dictionary sorted by a specific sort order.
#
# block - Ordering procedure.
#
# Returns +self+.
#
def order_by( &block )
@order_by = block
order
self
end
#
# Keep dictionary sorted by key.
#
# d = Dictionary.new.order_by_key
# d["z"] = 1
# d["y"] = 2
# d["x"] = 3
# d #=> {"x"=>3,"y"=>2,"z"=>2}
#
# This is equivalent to:
#
# Dictionary.new.order_by { |key,value| key }
#
# The initializer Dictionary#alpha also provides this.
#
# Returns +self+.
#
def order_by_key
if block_given?
@order_by = Proc.new{ |k,v| yield(k) }
else
@order_by = Proc.new{ |k,v| k }
end
order
self
end
#
# Keep dictionary sorted by value.
#
# d = Dictionary.new.order_by_value
# d["z"] = 1
# d["y"] = 2
# d["x"] = 3
# d #=> {"x"=>3,"y"=>2,"z"=>2}
#
# This is equivalent to:
#
# Dictionary.new.order_by { |key,value| value }
#
def order_by_value
if block_given?
@order_by = Proc.new{ |k,v| yield(v) }
else
@order_by = Proc.new{ |k,v| v }
end
order
self
end
#
# Re-apply the sorting procedure.
#
def reorder
if @order_by
assoc = @order.collect{ |k| [k,@hash[k]] }.sort_by(&@order_by)
@order = assoc.collect{ |k,v| k }
end
@order
end
#def ==( hsh2 )
# return false if @order != hsh2.order
# super hsh2
#end
#
# Is the dictionary instance equivalent to another?
#
def ==(hsh2)
if hsh2.is_a?( Dictionary )
@order == hsh2.order &&
@hash == hsh2.instance_variable_get("@hash")
else
false
end
end
#
# Lookup entry with key.
#
def [] key
@hash[ key ]
end
#
# Featch entry given +key+.
#
def fetch(key, *a, &b)
@hash.fetch(key, *a, &b)
end
#
# Store operator.
#
# h[key] = value
#
# Or with additional index.
#
# h[key,index] = value
#
def []=(k, i=nil, v=nil)
if v
insert(i,k,v)
else
store(k,i)
end
end
#
# Insert entry into dictionary at specific index position.
#
# index - [Integer] Position of order placement.
# key - [Object] Key to associate with value.
# value - [Object] Value to associate with key.
#
# Returns `value` stored.
#
def insert(index, key, value)
@order.insert(index, key)
@hash.store(key, value)
end
#
# Add entry into dictionary.
#
# Returns `value`.
#
def store(key, value)
@order.push(key) unless @hash.has_key?(key)
@hash.store(key, value)
end
#
# Clear dictionary of all entries.
#
def clear
@order = []
@hash.clear
end
#
# Delete the entry with given +key+.
#
def delete(key)
@order.delete(key)
@hash.delete(key)
end
#
# Iterate over each key.
#
def each_key
order.each { |k| yield( k ) }
self
end
#
# Iterate over each value.
#
def each_value
order.each { |k| yield( @hash[k] ) }
self
end
#
# Iterate over each key-value pair.
#
def each
order.each { |k| yield( k,@hash[k] ) }
self
end
alias each_pair each
#
# Delete entry if it fits conditional block.
#
def delete_if
order.clone.each { |k| delete k if yield(k,@hash[k]) }
self
end
#
# List of all dictionary values.
#
# Returns [Array].
#
def values
ary = []
order.each { |k| ary.push @hash[k] }
ary
end
#
# List of all dictionary keys.
#
# Returns [Array].
#
def keys
order
end
#
# Invert the dictionary.
#
# Returns [Dictionary] New dictionary that is inverse of the original.
#
def invert
hsh2 = self.class.new
order.each { |k| hsh2[@hash[k]] = k }
hsh2
end
#
# Reject entries based on give condition block and return
# new dictionary.
#
# Returns [Dictionary].
#
def reject(&block)
self.dup.delete_if(&block)
end
#
# Reject entries based on give condition block.
#
# Returns [Hash] of rejected entries.
#
# FIXME: This looks like it is implemented wrong!!!
#
def reject!( &block )
hsh2 = reject(&block)
self == hsh2 ? nil : hsh2
end
#
# Replace dictionary entries with new table.
#
def replace(hsh2)
case hsh2
when Dictionary
@order = hsh2.order
@hash = hsh2.to_h
when Hash
@hash = hsh2
@order = @hash.keys
else
@hash = hsh2.to_h
@order = @hash.keys
end
reorder
end
#
# Remove entry from the to top of dictionary.
#
def shift
key = order.first
key ? [key,delete(key)] : super
end
#
# Push entry on to the top of dictionary.
#
def unshift( k,v )
unless @hash.include?( k )
@order.unshift( k )
@hash.store( k,v )
true
else
false
end
end
#
# Same as #push.
#
def <<(kv)
push(*kv)
end
#
# Push entry on to bottom of the dictionary.
#
def push(k,v)
unless @hash.include?( k )
@order.push( k )
@hash.store( k,v )
true
else
false
end
end
#
# Pop entry off the bottom of dictionary.
#
def pop
key = order.last
key ? [key,delete(key)] : nil
end
#
# Inspection string for Dictionary.
#
# Returns [String].
#
def inspect
ary = []
each {|k,v| ary << k.inspect + "=>" + v.inspect}
'{' + ary.join(", ") + '}'
end
#
# Duplicate dictionary.
#
# Returns [Dictionary].
#
def dup
a = []
each{ |k,v| a << k; a << v }
self.class[*a]
end
#
# Update dictionary with other hash.
#
# Returns self.
#
def update( hsh2 )
hsh2.each { |k,v| self[k] = v }
reorder
self
end
alias :merge! update
#
# Merge other hash creating new dictionary.
#
# Returns [Dictionary].
#
def merge(hsh2)
self.dup.update(hsh2)
end
#
# Select items from dictiornary.
#
# Returns [Array] of two-element arrays.
#
def select
ary = []
each { |k,v| ary << [k,v] if yield k,v }
ary
end
#
# Reverse the order of the dictionary.
#
# Returns self.
#
def reverse!
@order.reverse!
self
end
#
# Reverse the order of duplicte dictionary.
#
# Returns [Dictionary].
#
def reverse
dup.reverse!
end
#
# Get/set initial entry value.
#
def first(x=nil)
return @hash[order.first] unless x
order.first(x).collect { |k| @hash[k] }
end
#
# Get/set last entry value.
#
def last(x=nil)
return @hash[order.last] unless x
order.last(x).collect { |k| @hash[k] }
end
#
# Number of items in the dictionary.
#
def length
@order.length
end
alias :size :length
#
# Is the dictionary empty?
#
# Returns `true` or `false`.
#
def empty?
@hash.empty?
end
#
# Does the dictionary have a given +key+.
#
# Returns `true` or `false`.
#
def has_key?(key)
@hash.has_key?(key)
end
#
# Does the dictionary have a given +key+.
#
# Returns `true` or `false`.
#
def key?(key)
@hash.key?(key)
end
#
# Convert to array.
#
# Returns [Array] of two-element arrays.
#
def to_a
ary = []
each { |k,v| ary << [k,v] }
ary
end
#
# Convert to array then to string.
#
# Returns [String].
#
def to_s
self.to_a.to_s
end
#
# Get a duplicate of the underlying hash table.
#
# Returns [Hash].
#
def to_hash
@hash.dup
end
#
# Get a duplicate of the underlying hash table.
#
# Returns [Hash].
#
def to_h
@hash.dup
end
protected
#
# Underlying hash table.
#
def hash_table
@hash
end
end
end
@@ -0,0 +1,206 @@
require 'set'
module Hashery
# FuzzyHash is a weird hash with special semantics for regex keys.
#
# This is useful when you want to have a lookup table that can either contain strings or regexes.
# For instance, you might want a catch all for certain regexes that perform a certain logic.
#
# >> hash = FuzzyHash.new
# >> hash[/^\d+$/] = 'number'
# >> hash[/.*/] = 'something'
# >> hash['chunky'] = 'bacon'
# >> hash['foo'] = 'vader'
#
# >> hash['foo']
# << 'vader'
# >> hash['food']
# << 'something'
# >> hash['123']
# << 'number'
#
# This class is based on Joshua Hull's original FuzzyHash class.
#
class FuzzyHash
#
#
#
def initialize(init_hash = nil)
@fuzzies = []
@hash_reverse = {}
@fuzzies_reverse = {}
@fuzzy_hash = {}
@hash = {}
init_hash.each{ |key,value| self[key] = value } if init_hash
end
#
#
#
def clear
hash.clear
fuzzies.clear
hash_reverse.clear
fuzzies_reverse.clear
end
#
#
#
def size
hash.size + fuzzies.size
end
alias_method :count, :size
#
#
#
def ==(o)
o.is_a?(FuzzyHash)
o.send(:hash) == hash &&
o.send(:fuzzies) == fuzzies
end
#
#
#
def empty?
hash.empty? && fuzzies.empty?
end
#
#
#
def keys
hash.keys + fuzzy_hash.keys
end
#
#
#
def values
hash.values + fuzzies.collect{|r| r.last}
end
#
#
#
def each
hash.each{|k,v| yield k,v }
fuzzies.each{|v| yield v.first, v.last }
end
#
#
#
def delete_value(value)
hash.delete(hash_reverse[value]) || ((rr = fuzzies_reverse[value]) && fuzzies.delete_at(rr[0]))
end
#
#
#
def []=(key, value)
if Regexp === key
fuzzies.delete_if{|f| f.first.inspect.hash == key.inspect.hash}
fuzzies_reverse.delete_if{|k, v| v[1].inspect.hash == key.inspect.hash}
hash_reverse.delete_if{|k,v| v.inspect.hash == key.inspect.hash}
fuzzy_hash[key] = value
fuzzies << [key, value]
reset_fuzz_test!
fuzzies_reverse[value] = [fuzzies.size - 1, key, value]
else
hash[key] = value
hash_reverse.delete_if{|k,v| v.hash == key.hash}
hash_reverse[value] = key
end
value
end
#
#
#
def replace(src, dest)
if hash_reverse.key?(src)
key = hash_reverse[src]
hash[key] = dest
hash_reverse.delete(src)
hash_reverse[dest] = key
elsif fuzzies_reverse.key?(src)
key = fuzzies_reverse[src]
fuzzies[rkey[0]] = [rkey[1], dest]
fuzzies_reverse.delete(src)
fuzzies_reverse[dest] = [rkey[0], rkey[1], dest]
end
end
#
#
#
def [](key)
(hash.key?(key) && hash[key]) ||
((lookup = fuzzy_lookup(key)) && lookup && lookup.first) ||
fuzzy_hash[key]
end
#
#
#
def match_with_result(key)
if hash.key?(key)
[hash[key], key]
else
fuzzy_lookup(key)
end
end
private
attr_reader :fuzzies, :hash_reverse, :fuzzies_reverse, :hash, :fuzzy_hash
attr_writer :fuzz_test
#
#
#
def reset_fuzz_test!
self.fuzz_test = nil
end
#
#
#
def fuzz_test
unless @fuzz_test
@fuzz_test = Object.new
@fuzz_test.instance_variable_set(:'@fuzzies', fuzzies)
method = "
def match(str)
case str\n
"
fuzzies.each_with_index do |reg, index|
method << "when #{reg.first.inspect}; [@fuzzies[#{index}][1], Regexp.last_match(0)];"
end
method << "end\nend\n"
@fuzz_test.instance_eval method
end
@fuzz_test
end
#
#
#
def fuzzy_lookup(key)
if !fuzzies.empty? && (value = fuzz_test.match(key))
value
end
end
end
end
# Copyright (c) 2009 Joshua Hull
@@ -0,0 +1,321 @@
module Hashery
# Hash class with methods to read from and write into ini files.
#
# A ini file is a text file in a specific format,
# it may include several fields which are sparated by
# field headlines which are enclosured by "[]".
# Each field may include several key-value pairs.
#
# Each key-value pair is represented by one line and
# the value is sparated from the key by a "=".
#
# == Examples
#
# === Example ini file
#
# # this is the first comment which will be saved in the comment attribute
# mail=info@example.com
# domain=example.com # this is a comment which will not be saved
# [database]
# db=example
# user=john
# passwd=very-secure
# host=localhost
# # this is another comment
# [filepaths]
# tmp=/tmp/example
# lib=/home/john/projects/example/lib
# htdocs=/home/john/projects/example/htdocs
# [ texts ]
# wellcome=Wellcome on my new website!
# Website description = This is only a example. # and another comment
#
# === Example object
#
# Ini#comment stores:
#
# "this is the first comment which will be saved in the comment attribute"
#
# Ini's internal hash stores:
#
# {
# "mail" => "info@example.com",
# "domain" => "example.com",
# "database" => {
# "db" => "example",
# "user" => "john",
# "passwd" => "very-secure",
# "host" => "localhost"
# },
# "filepaths" => {
# "tmp" => "/tmp/example",
# "lib" => "/home/john/projects/example/lib",
# "htdocs" => "/home/john/projects/example/htdocs"
# }
# "texts" => {
# "wellcome" => "Wellcome on my new website!",
# "Website description" => "This is only a example."
# }
# }
#
# As you can see this module gets rid of all comments, linebreaks
# and unnecessary spaces at the beginning and the end of each
# field headline, key or value.
#
# === Using the object
#
# Using the object is stright forward:
#
# ini = IniHash.new("path/settings.ini")
# ini["mail"] = "info@example.com"
# ini["filepaths"] = { "tmp" => "/tmp/example" }
# ini.comment = "This is\na comment"
# puts ini["filepaths"]["tmp"]
# # => /tmp/example
# ini.write()
#
# == Acknowlegements
#
# IniHash is based on ini.rb.
#
# Copyright (C) 2007 Jeena Paradies <info@jeenaparadies.net>
class IniHash
# TODO: Use class method for loading from file, not initializer.
#
# NOTE: In future versions, `#new` will not take a path, and `#load`
# will have to be used.
#
def self.load(path, load=true)
new(path, load)
end
#
# The hash which holds all INI data.
#
attr_accessor :inihash
#
# The string which holds the comments on the top of the file
#
attr_accessor :comment
#
# Creating a new IniHash object.
#
# path - is a path to the ini file
# load - if nil restores the data if possible
# if true restores the data, if not possible raises an error
# if false does not resotre the data
#
def initialize(path, load=nil)
@path = path if String === path
@inihash = (Hash === path ? path.dup : {})
if load or ( load.nil? and FileTest.readable_real? @path )
restore()
end
end
#
# Retrive the ini data for the key +key+
#
def [](key)
@inihash[key]
end
#
# Set the ini data for the key +key+
#
# key - Index key.
# value - The value to index.
#
# Returns +value+.
#
def []=(key, value)
#raise TypeError, "String expected" unless key.is_a? String
key = key.to_str
#raise TypeError, "String or Hash expected" unless value.is_a? String or value.is_a? Hash
value = value.to_str unless Hash === value
@inihash[key] = value
end
#
# Restores the data from file into the object
#
def restore
@inihash = IniHash.read_from_file(@path)
@comment = IniHash.read_comment_from_file(@path)
end
#
# Store data from the object in the file.
#
def save
IniHash.write_to_file(@path, @inihash, @comment)
end
#
# Deprecated: Save INI data to file path. Use #save instead.
#
def update
warn 'IniHash#update is deprecated for this use, use IniHash#save instead.'
save
end
#
# Convert to hash by duplicating the underlying hash table.
#
def to_h
@inihash.dup
end
alias :inspect :to_s
#
# Turn a hash (up to 2 levels deepness) into a ini string
#
# inihash - Hash representing the ini File. Default is a empty hash.
#
# Returns a string in the ini file format.
#
def to_s
str = ""
inihash.each do |key, value|
if value.is_a? Hash
str << "[#{key.to_s}]\n"
value.each do |under_key, under_value|
str << "#{under_key.to_s}=#{under_value.to_s unless under_value.nil?}\n"
end
else
str << "#{key.to_s}=#{value.to_s unless value.nil?}\n"
end
end
str
end
#
# Delegate missing mthods to underlying Hash.
#
# TODO: Sublcass Hash instead of delegating.
#
def method_missing(s,*a,&b)
@inihash.send(s, *a, &b) if @inihash.respond_to?(s)
end
#
# Reading data from file
#
# path - a path to the ini file
#
# Returns a `Hash` which represents the data from the file.
#
def self.read_from_file(path)
raise "file not found - #{path}" unless File.file?(path)
inihash = {}
headline = nil
IO.foreach(path) do |line|
line = line.strip.split(/#/)[0].to_s
# read it only if the line doesn't begin with a "=" and is long enough
unless line.length < 2 and line[0,1] == "="
# it's a headline if the line begins with a "[" and ends with a "]"
if line[0,1] == "[" and line[line.length - 1, line.length] == "]"
# get rid of the [] and unnecessary spaces
headline = line[1, line.length - 2 ].strip
inihash[headline] = {}
else
key, value = line.split(/=/, 2)
key = key.strip unless key.nil?
value = value.strip unless value.nil?
unless headline.nil?
inihash[headline][key] = value
else
inihash[key] = value unless key.nil?
end
end
end
end
inihash
end
#
# Reading comments from file
#
# path - a path to the INI file
#
# Returns a `String` with the comments from the beginning of the INI file.
#
def self.read_comment_from_file(path)
comment = ""
IO.foreach(path) do |line|
line.strip!
break unless line[0,1] == "#" or line == ""
comment_line = line[1, line.length].to_s
comment << "#{comment_line.strip}\n"
end
comment
end
#
# Writing a ini hash into a file
#
# path - Path to the INI file.
# inihash - Hash representing the ini File. Default is a empty hash.
# comment - String with comments which appear on the
# top of the file. Each line will get a "#" before.
# Default is no comment.
#
def self.write_to_file(path, inihash={}, comment=nil)
raise TypeError, "String expected" unless comment.is_a? String or comment.nil?
raise TypeError, "Hash expected" unless inihash.is_a? Hash
File.open(path, "w") { |file|
unless comment.nil?
comment.each do |line|
file << "# #{line}"
end
end
file << IniHash.text(inihash)
}
end
#
# Turn a hash (up to 2 levels deepness) into a ini string
#
# inihash - Hash representing the ini File. Default is a empty hash.
#
# Returns a String in the ini file format.
#
# TODO: Rename `IniHash.text` method to something else ?
#
def self.text(inihash={})
new(inihash).to_s
end
class << self
# @deprecated
alias_method :to_s, :text
end
end
end
@@ -0,0 +1,61 @@
require 'hashery/crud_hash'
module Hashery
# The KeyHash class is a Hash class which accepts a block for
# normalizing keys.
#
# The KeyHash class is essentially the same as a normal Hash.
# But notice the significant distinction of indifferent key
# access.
#
# s = KeyHash.new
# s[:x] = 1
# s[:x] #=> 1
# s['x'] #=> 1
#
# We can see that internally the key has indeed been converted
# to a String.
#
# s.to_h #=> {'x'=>1 }
#
# By default all keys are converted to strings. This has two advantages
# over a regular Hash is many usecases. First it means hash entries have
# indifferent access. <tt>1</tt>, <tt>"1"</tt> and <tt>:1</tt> are all
# equivalent --any object that defines <tt>#to_s</tt> can be used as a key.
# Secondly, since strings are garbage collected so will default KeyHash
# objects.
#
# But keys can be normalized by any function. Theses functions can be quite
# unique.
#
# h = KeyHash.new(0){ |k| k.to_i }
# h[1.34] += 1
# h[1.20] += 1
# h[1.00] += 1
# h #=> { 1 => 3 }
#
class KeyHash < CRUDHash
#
# Unlike a regular Hash, a KeyHash's block sets the `key_proc` rather
# than the `default_proc`.
#
def initialize(*default, &block)
super(*default)
@key_proc = block || Proc.new{ |k| k.to_s }
end
end
end
#class Hash
# #
# # Convert a Hash to a KeyHash object.
# #
# def to_keyhash
# Hashery::KeyHash[self]
# end
#end
@@ -0,0 +1,249 @@
require 'enumerator'
module Hashery
# LinkedList implements a simple doubly linked list with efficient
# hash-like element access.
#
# This is a simple linked-list implementation with efficient random
# access of data elements. It was inspired by George Moscovitis'
# LRUCache implementation found in Facets 1.7.30, but unlike the
# linked-list in that cache, this one does not require the use of a
# mixin on any class to be stored. The linked-list provides the
# push, pop, shift, unshift, first, last, delete and length methods
# which work just like their namesakes in the Array class, but it
# also supports setting and retrieving values by key, just like a
# hash.
#
# LinkedList was ported from the original in Kirk Hanes IOWA web framework.
#
# == Acknowledgements
#
# LinkedList is based on the LinkedList library by Kirk Haines.
#
# Copyright (C) 2006 Kirk Haines <khaines@enigo.com>.
#
class LinkedList
include Enumerable
# Represents a single node of the linked list.
#
class Node
attr_accessor :key, :value, :prev_node, :next_node
def initialize(key=nil,value=nil,prev_node=nil,next_node=nil)
@key = key
@value = value
@prev_node = prev_node
@next_node = next_node
end
end
#
# Initialize new LinkedList instance.
#
def initialize
@head = Node.new
@tail = Node.new
@lookup = Hash.new
node_join(@head,@tail)
end
#
# Lookup entry by key.
#
def [](key)
@lookup[key].value
end
#
# Add node to linked list.
#
def []=(k,v)
if @lookup.has_key?(k)
@lookup[k].value = v
else
n = Node.new(k,v,@head,@head.next_node)
node_join(n,@head.next_node)
node_join(@head,n)
@lookup[k] = n
end
v
end
#
# Is linked list empty?
#
def empty?
@lookup.empty?
end
#
# Remove node idenified by key.
#
def delete(key)
n = @lookup.delete(key)
v = n ? node_purge(n) : nil
v
end
#
# Get value of first node.
#
def first
@head.next_node.value
end
#
# Get value of last node.
#
def last
@tail.prev_node.value
end
#
#
#
def shift
k = @head.next_node.key
n = @lookup.delete(k)
node_delete(n) if n
end
#
#
#
def unshift(v)
if @lookup.has_key?(v)
n = @lookup[v]
node_delete(n)
node_join(n,@head.next_node)
node_join(@head,n)
else
n = Node.new(v,v,@head,@head.next_node)
node_join(n,@head.next_node)
node_join(@head,n)
@lookup[v] = n
end
v
end
#
#
#
def pop
k = @tail.prev_node.key
n = @lookup.delete(k)
node_delete(n) if n
end
#
#
#
def push(v)
if @lookup.has_key?(v)
n = @lookup[v]
node_delete(n)
node_join(@tail.prev_node,n)
node_join(n,@tail)
else
n = Node.new(v,v,@tail.prev_node,@tail)
node_join(@tail.prev_node,n)
node_join(n,@tail)
@lookup[v] = n
end
v
end
alias :<< :push
#
# Produces an Array of key values.
#
# Returns [Array].
#
def queue
r = []
n = @head
while (n = n.next_node) and n != @tail
r << n.key
end
r
end
#
# Converts to an Array of node values.
#
# Returns [Array].
#
def to_a
r = []
n = @head
while (n = n.next_node) and n != @tail
r << n.value
end
r
end
#
# Number of nodes.
#
def length
@lookup.length
end
alias size length
#
# Iterate over nodes, starting with the head node
# and ending with the tail node.
#
def each
n = @head
while (n = n.next_node) and n != @tail
yield(n.key,n.value)
end
end
private
#
# Delete a node.
#
# n - A node.
#
def node_delete(n)
node_join(n.prev_node,n.next_node)
v = n.value
end
#
# Purge a node.
#
# n - A node.
#
def node_purge(n)
node_join(n.prev_node,n.next_node)
v = n.value
n.value = nil
n.key = nil
n.next_node = nil
n.prev_node = nil
v
end
# Join two nodes.
#
# a - A node.
# b - A node.
#
def node_join(a,b)
a.next_node = b
b.prev_node = a
end
end
end
@@ -0,0 +1,363 @@
require 'enumerator'
module Hashery
# Hash with LRU expiry policy. There are at most max_size elements in a
# LRUHash. When adding more elements old elements are removed according
# to LRU policy.
#
# Based on Robert Klemme's LRUHash class.
#
# LRUHash, Copyright (c) 2010 Robert Klemme.
#
class LRUHash
include Enumerable
attr_reader :max_size
attr_accessor :default
attr_accessor :default_proc
attr_accessor :release_proc
#
# Initialize new LRUHash instance.
#
# max_size -
# default_value -
# block -
#
def initialize(max_size, default_value=nil, &block)
@max_size = normalize_max(max_size)
@default = default_value
@default_proc = block
@h = {}
@head = Node.new
@tail = front(Node.new)
end
#
# Iterate over each pair.
#
def each_pair
if block_given?
each_node do |n|
yield [n.key, n.value]
end
else
enum_for :each_pair
end
end
#
# Same as each pair.
#
alias each each_pair
#
# Iterate over each key.
#
def each_key
if block_given?
each_node do |n|
yield n.key
end
else
enum_for :each_key
end
end
#
# Iterate over each value.
#
def each_value
if block_given?
each_node do |n|
yield n.value
end
else
enum_for :each_value
end
end
#
# Size of the hash.
#
def size
@h.size
end
#
#
#
def empty?
@head.succ.equal? @tail
end
#
#
#
def fetch(key, &b)
n = @h[key]
if n
front(n).value
else
(b || FETCH)[key]
end
end
#
#
#
def [](key)
fetch(key) do |k|
@default_proc ? @default_proc[self, k] : default
end
end
#
#
#
def keys
@h.keys
end
#
#
#
def values
@h.map {|k,n| n.value}
end
#
#
#
def has_key?(key)
@h.has_key? key
end
alias key? has_key?
alias member? has_key?
alias include? has_key?
#
#
#
def has_value?(value)
each_pair do |k, v|
return true if value.eql? v
end
false
end
alias value? has_value?
def values_at(*key_list)
key_list.map {|k| self[k]}
end
#
#
#
def assoc(key)
n = @h[key]
if n
front(n)
[n.key, n.value]
end
end
#
#
#
def rassoc(value)
each_node do |n|
if value.eql? n.value
front(n)
return [n.key, n.value]
end
end
nil
end
#
#
#
def key(value)
pair = rassoc(value) and pair.first
end
#
#
#
def store(key, value)
# same optimization as in Hash
key = key.dup.freeze if String === key && !key.frozen?
n = @h[key]
unless n
if size == max_size
# reuse node to optimize memory usage
n = delete_oldest
n.key = key
n.value = value
else
n = Node.new key, value
end
@h[key] = n
end
front(n).value = value
end
alias []= store
#
#
#
def delete(key)
n = @h[key] and remove_node(n).value
end
#
#
#
def delete_if
each_node do |n|
remove_node n if yield n.key, n.value
end
end
#
#
#
def max_size=(limit)
limit = normalize_max(limit)
while size > limit
delete_oldest
end
@max_size = limit
end
#
#
#
def clear
until empty?
delete_oldest
end
self
end
#
#
#
def to_s
s = nil
each_pair {|k, v| (s ? (s << ', ') : s = '{') << k.to_s << '=>' << v.to_s}
s ? (s << '}') : '{}'
end
alias inspect to_s
private
#
# Iterate nodes.
#
def each_node
n = @head.succ
until n.equal? @tail
succ = n.succ
yield n
n = succ
end
self
end
#
# Move node to front.
#
# node - [Node]
#
def front(node)
node.insert_after(@head)
end
#
# Remove the node and invoke release_proc
# if set
#
# node - [Node]
#
def remove_node(node)
n = @h.delete(node.key)
n.unlink
release_proc and release_proc[n.key, n.value]
n
end
#
# Remove the oldest node returning the node
#
def delete_oldest
n = @tail.pred
raise "Cannot delete from empty hash" if @head.equal? n
remove_node n
end
#
# Normalize the argument in order to be usable as max_size
# criterion is that n.to_i must be an Integer and it must
# be larger than zero.
#
# n - [#to_i] max size
#
def normalize_max(n)
n = n.to_i
raise ArgumentError, 'Invalid max_size: %p' % n unless Integer === n && n > 0
n
end
#
FETCH = Proc.new {|k| raise KeyError, 'key not found'}
# A single node in the doubly linked LRU list of nodes.
Node = Struct.new :key, :value, :pred, :succ do
def unlink
pred.succ = succ if pred
succ.pred = pred if succ
self.succ = self.pred = nil
self
end
def insert_after(node)
raise 'Cannot insert after self' if equal? node
return self if node.succ.equal? self
unlink
self.succ = node.succ
self.pred = node
node.succ.pred = self if node.succ
node.succ = self
self
end
end
end
end
@@ -0,0 +1,167 @@
require 'hashery/open_hash'
module Hashery
# OpenCascade is subclass of OpenHash. It differs in a few
# significant ways. The reason this class is called "cascade" is that
# every internal Hash is transformed into an OpenCascade dynamically
# upon access. This makes it easy to create "cascading" references.
#
# h = { :x => { :y => { :z => 1 } } }
# c = OpenCascade[h]
# c.x.y.z #=> 1
#
# As soon as you access a node it automatically becomes an OpenCascade.
#
# c = OpenCascade.new #=> #<OpenCascade:0x7fac3680ccf0 {}>
# c.r #=> #<OpenCascade:0x7fac368084c0 {}>
# c.a.b #=> #<OpenCascade:0x7fac3680a4f0 {}>
#
# But if you set a node, then that will be that value.
#
# c.a.b = 4 #=> 4
#
# To query a node without causing the auto-creation of an OpenCasade
# instance, use the `?`-mark.
#
# c.a.z? #=> nil
#
# OpenCascade also transforms Hashes within Arrays.
#
# h = { :x=>[ {:a=>1}, {:a=>2} ], :y=>1 }
# c = OpenCascade[h]
# c.x.first.a.assert == 1
# c.x.last.a.assert == 2
#
# Finally, you can set call a private method via bang methods using the `!`-mark.
#
# c = OpenCascade.new #=> #<OpenCascade:0x7fac3680ccf0 {}>
# c.each = 4
# c.each! do |k,v|
# ...
# end
#
# c.x!(4).y!(3) #=> #<OpenCascade:0x7fac3680ccf0 {:x=>4, :y=>3}>
#
# Subclassing OpenCascade with cause the new subclass to become the class that
# is auto-created. If this is not the behavior desired, consider using delegation
# instead of subclassing.
#
class OpenCascade < OpenHash
#
#def self.[](hash)
# oc = new
# hash.each{ |(k,v)| oc.store(k,v) }
# oc
#end
#
# Initialize new OpenCascade instance.
#
# default - The usual default object.
#
def initialize(*default)
@read = {}
leet = lambda { |h,k| h[k] = self.class.new(&leet) }
super(*default, &leet)
end
#
# Alias for original read method.
#
alias :retrieve! :retrieve
#
# Read value given a +key+.
#
# key - Index key to lookup.
#
# Returns value.
#
def retrieve(key)
ckey = cast_key(key)
if @read[ckey]
super(key)
else
@read[ckey] = store(key, cast_value(super(key)))
end
end
#
#
#
def method_missing(sym, *args, &blk)
type = sym.to_s[-1,1]
name = sym.to_s.gsub(/[=!?]$/, '').to_sym
case type
when '='
store(name, args.first)
when '?'
key?(name) ? retrieve!(name) : nil # key?(name)
when '!'
__send__(name, *args, &blk)
else
#if key?(name)
retrieve(name)
#else
# #default = OpenCascade.new #self.class.new
# #default = default_proc ? default_proc.call(self, name) : default
# store(name, read(name))
#end
end
end
def respond_to?(sym, include_private = false)
sym != :to_ary && super
end
#def each
# super do |key, entry|
# yield([key, transform_entry(entry)])
# end
#end
private
#
# Cast value, such that Hashes are converted to OpenCascades.
# And Hashes in Arrays are converted to OpenCascades as well.
#
def cast_value(entry)
case entry
when Hash
e = OpenCascade.new
e.key_proc = key_proc if key_proc
e.merge!(entry)
e
when Array
entry.map{ |e| cast_value(e) }
else
entry
end
end
end
end
#--
# Last, when an entry is not found, 'null' is returned rather then 'nil'.
# This allows for run-on entries withuot error. Eg.
#
# o = OpenCascade.new
# o.a.b.c #=> null
#
# Unfortuately this requires an explict test for null? in 'if' conditions.
#
# if o.a.b.c.null? # true if null
# if o.a.b.c.nil? # true if nil or null
# if o.a.b.c.not? # true if nil or null or false
#
# So be sure to take that into account.
#++
@@ -0,0 +1,145 @@
require 'hashery/crud_hash'
module Hashery
# OpenHash is a Hash, but also supports open properties much like
# OpenStruct.
#
# Only names that are name methods of Hash can be used as open slots.
# To open a slot for a name that would otherwise be a method, the
# method needs to be made private. The `#open!` method can be used
# to handle this.
#
# Examples
#
# o = OpenHash.new
# o.open!(:send)
# o.send = 4
#
class OpenHash < CRUDHash
alias :object_class :class
#FILTER = /(^__|^\W|^instance_|^object_|^to_)/
#methods = Hash.instance_methods(true).select{ |m| m !~ FILTER }
#methods = methods - [:each, :inspect, :send] # :class, :as]
#private *methods
#
# Initialize new OpenHash instance.
#
# TODO: Maybe `safe` should be the first argument?
#
def initialize(default=nil, safe=false, &block)
@safe = safe
super(*[default].compact, &block)
end
#
# If safe is set to true, then public methods cannot be overriden
# by hash keys.
#
attr_accessor :safe
#
# Alias to original store method.
#
#alias :store! :store
#
# Index `value` to `key`. Unless safe mode, will also open up the
# key if it is not already open.
#
# key - Index key to associate with value.
# value - Value to be associate with key.
#
# Returns +value+.
#
def store(key, value)
#open!(key)
super(key, value)
end
#
# Open up a slot that that would normally be a Hash method.
#
# The only methods that can't be opened are ones starting with `__`.
#
# methods - [Array<String,Symbol>] method names
#
# Returns Array of slot names that were opened.
#
def open!(*methods)
# Only select string and symbols, any other type of key is allowed,
# it just won't be accessible via dynamic methods.
methods = methods.select{ |x| String === x || Symbol === x }
if methods.any?{ |m| m.to_s.start_with?('__') }
raise ArgumentError, "cannot open shadow methods"
end
# only public methods need be made protected
methods = methods.map{ |x| x.to_sym }
methods = methods & public_methods(true).map{ |x| x.to_sym }
if @safe
raise ArgumentError, "cannot set public method" unless methods.empty?
else
(class << self; self; end).class_eval{ protected *methods }
end
methods
end
# @deprecated
alias :omit! :open!
#
# Is a slot open?
#
# method - [String,Symbol] method name
#
# Returns `true` or `false`.
#
def open?(method)
methods = public_methods(true).map{ |m| m.to_sym }
! methods.include?(method.to_sym)
end
#
# Make specific Hash methods available for use that have previously opened.
#
# methods - [Array<String,Symbol>] method names
#
# Returns +methods+.
#
def close!(*methods)
(class << self; self; end).class_eval{ public *methods }
methods
end
#
#
#
def method_missing(s,*a, &b)
type = s.to_s[-1,1]
name = s.to_s.sub(/[!?=]$/, '')
key = name.to_sym
case type
when '='
store(key, a.first)
when '?'
key?(key)
when '!'
# call an underlying private method
# TODO: limit this to omitted methods (from included) ?
__send__(name, *a, &b)
else
#if key?(key)
retrieve(key)
#else
# super(s,*a,&b)
#end
end
end
end
end
@@ -0,0 +1,174 @@
module Hashery
# OrderedHash is a simple ordered hash implmentation, for users of
# Ruby 1.8.7 or less.
#
# NOTE: As of Ruby 1.9+ this class is not needed, since
# Ruby 1.9's standard Hash tracks inseration order.
#
# This implementation derives from the same class in
# ActiveSupport library.
#
class OrderedHash < ::Hash
def to_yaml_type
"!tag:yaml.org,2002:omap"
end
def to_yaml(opts = {})
YAML.quick_emit(self, opts) do |out|
out.seq(taguri, to_yaml_style) do |seq|
each do |k, v|
seq.add(k => v)
end
end
end
end
# Hash is ordered in Ruby 1.9!
if RUBY_VERSION < '1.9'
def initialize(*args, &block)
super
@keys = []
end
def self.[](*args)
ordered_hash = new
if (args.length == 1 && args.first.is_a?(Array))
args.first.each do |key_value_pair|
next unless (key_value_pair.is_a?(Array))
ordered_hash[key_value_pair[0]] = key_value_pair[1]
end
return ordered_hash
end
unless (args.size % 2 == 0)
raise ArgumentError.new("odd number of arguments for Hash")
end
args.each_with_index do |val, ind|
next if (ind % 2 != 0)
ordered_hash[val] = args[ind + 1]
end
ordered_hash
end
def initialize_copy(other)
super(other)
@keys = other.keys
end
def []=(key, value)
@keys << key unless key?(key)
super(key, value)
end
def delete(key)
if has_key? key
index = @keys.index(key)
@keys.delete_at(index)
end
super(key)
end
def delete_if
super
sync_keys!
self
end
def reject!
super
sync_keys!
self
end
def reject(&block)
dup.reject!(&block)
end
def keys
@keys.dup
end
def values
@keys.collect{ |key| self[key] }
end
def to_hash
self
end
def to_a
@keys.map{ |key| [ key, self[key] ] }
end
def each_key
@keys.each{ |key| yield(key) }
end
def each_value
@keys.each{ |key| yield(self[key]) }
end
def each
@keys.each{ |key| yield(key, self[key]) }
end
alias_method :each_pair, :each
def clear
super
@keys.clear
self
end
def shift
k = @keys.first
v = delete(k)
[k, v]
end
def merge!(other_hash)
other_hash.each{ |k,v| self[k] = v }
self
end
def merge(other_hash)
dup.merge!(other_hash)
end
# When replacing with another hash, the initial order of our
# keys must come from the other hash, ordered or not.
def replace(other)
super
@keys = other.keys
self
end
def inspect
"#<OrderedHash #{super}>"
end
private
def sync_keys!
@keys.delete_if{ |k| !key?(k) }
end
end
end
end
require 'yaml'
YAML.add_builtin_type("omap") do |type, val|
OrderedHash[val.map(&:to_a).map(&:first)]
end
@@ -0,0 +1,238 @@
module Hashery
# A PathHash is a hash whose values can be accessed in the normal manner,
# or with keys that are slash (`/`) separated strings. To get the whole hash
# as a single flattened level, call `#flat`. All keys are converted to strings.
# All end-of-the-chain values are kept in whatever value they are.
#
# s = PathHash['a' => 'b', 'c' => {'d' => :e}]
# s['a'] #=> 'b'
# s['c'] #=> {slashed: 'd'=>:e}
# s['c']['d'] #=> :e
# s['c/d'] #=> :e
#
# PathHash is derived from the SlashedHash class in the HashMagic project
# by Daniel Parker <gems@behindlogic.com>.
#
# Copyright (c) 2006 BehindLogic (http://hash_magic.rubyforge.org)
#
# Authors: Daniel Parker
#
# TODO: This class is very much a work in progess and will be substantially rewritten
# for future versions.
#
class PathHash < Hash
#
# Initialize PathHash.
#
# hsh - Priming Hash.
#
def initialize(hsh={})
raise ArgumentError, "must be a hash or array of slashed values" unless hsh.is_a?(Hash) || hsh.is_a?(Array)
@constructor = hsh.is_a?(Hash) ? hsh.class : Hash
@flat = flatten_to_hash(hsh)
end
# Standard Hash methods, plus the overwritten ones
#include StandardHashMethodsInRuby
# Behaves like the usual Hash#[] method, but you can access nested hash
# values by composing a single key of the traversing keys joined by '/':
#
# hash['c']['d'] # is the same as:
# hash['c/d']
#
def [](key)
rg = Regexp.new("^#{key}/?")
start_obj = if @constructor == OrderedHash
@constructor.new((@flat.instance_variable_get(:@keys_in_order) || []).collect {|e| e.gsub(rg,'')})
else
@constructor.new
end
v = @flat.has_key?(key) ? @flat[key] : self.class.new(@flat.reject {|k,v| !(k == key || k =~ rg)}.inject(start_obj) {|h,(k,v)| h[k.gsub(rg,'')] = v; h})
v.is_a?(self.class) && v.empty? ? nil : v
end
#
# Same as above, except sets value rather than retrieving it.
#
def []=(key,value)
@flat.reject! {|k,v| k == key || k =~ Regexp.new("^#{key}/")}
if value.is_a?(Hash)
flatten_to_hash(value).each do |hk,hv|
@flat[key.to_s+'/'+hk.to_s] = hv
end
else
@flat[key.to_s] = value
end
end
def clear # :nodoc:
@flat.clear
end
#
#
#
def fetch(key,default=:ehisehoah0928309q98y30,&block) # :nodoc:
value = @flat.has_key?(key) ? @flat[key] : self.class.new(@flat.reject {|k,v| !(k == key || k =~ Regexp.new("^#{key}/"))}.inject({}) {|h,(k,v)| h[k.split('/',2)[1]] = v; h})
if value.is_a?(self.class) && value.empty?
if default == :ehisehoah0928309q98y30
if block_given?
block.call(key)
else
raise IndexError
end
value
else
default
end
else
value
end
end
#
# Delete entry from Hash. Slashed keys can be used here, too.
#
# key - The key to delete.
# block - Produces the return value if key not found.
#
# Returns delete value.
#
def delete(key,&block)
value = @flat.has_key?(key) ? @flat[key] : self.class.new(@flat.reject {|k,v| !(k == key || k =~ Regexp.new("^#{key}/"))}.inject({}) {|h,(k,v)| h[k.split('/',2)[1]] = v; h})
return block.call(key) if value.is_a?(self.class) && value.empty? && block_given?
@flat.keys.reject {|k| !(k == key || k =~ Regexp.new("^#{key}/"))}.each {|k| @flat.delete(k)}
return value
end
#
def empty?
@flat.empty?
end
# This gives you the slashed key of the value, no matter where the value is in the tree.
def index(value)
@flat.index(value)
end
#
def inspect
@flat.inspect.insert(1,'slashed: ')
end
# This gives you only the top-level keys, no slashes. To get the list of slashed keys, do hash.flat.keys
def keys
@flat.inject([]) {|a,(k,v)| a << [k.split('/',2)].flatten[0]; a}.uniq
end
# This is rewritten to mean something slightly different than usual: Use this to restructure the hash, for cases when you
# end up with an array holding several hashes.
def rehash # :nodoc:
@flat.rehash
end
# Gives a list of all keys in all levels in the multi-level hash, joined by slashes.
#
# {'a'=>{'b'=>'c', 'c'=>'d'}, 'b'=>'c'}.slashed.flat.keys
# #=> ['a/b', 'a/c', 'b']
#
def flat
@flat
end
# Expands the whole hash to Hash objects ... not useful very often, it seems.
def expand
inject({}) {|h,(k,v)| h[k] = v.is_a?(SlashedHash) ? v.expand : v; h}
end
def to_string_array
flatten_to_array(flat,[])
end
def slashed # :nodoc:
self
end
# Same as ordered! but returns a new SlashedHash object instead of modifying the same.
def ordered(*keys_in_order)
dup.ordered!(*keys_in_order)
end
# Sets the SlashedArray as ordered. The *keys_in_order must be a flat array
# of slashed keys that specify the order for each level:
#
# s = {'a'=>{'b'=>'c', 'c'=>'d'}, 'b'=>'c'}.slashed
# s.ordered!('b', 'a/c', 'a/b')
# s.expand # => {'b'=>'c', 'a'=>{'c'=>'d', 'b'=>'c'}}
# # Note that the expanded hashes will *still* be ordered!
#
def ordered!(*keys_in_order)
return self if @constructor == OrderedHash
@constructor = OrderedHash
@flat = @flat.ordered(*keys_in_order)
self
end
#
def ==(other)
case other
when SlashedHash
@slashed == other.instance_variable_get(:@slashed)
when Hash
self == SlashedHash.new(other)
else
raise TypeError, "Cannot compare #{other.class.name} with SlashedHash"
end
end
private
def flatten_to_hash(hsh)
flat = @constructor.new
if hsh.is_a?(Array)
hsh.each do |e|
flat.merge!(flatten_to_hash(e))
end
elsif hsh.is_a?(Hash)
hsh.each do |k,v|
if v.is_a?(Hash)
flatten_to_hash(v).each do |hk,hv|
flat[k.to_s+'/'+hk.to_s] = hv
end
else
flat[k.to_s] = v
end
end
else
ks = hsh.split('/',-1)
v = ks.pop
ks = ks.join('/')
if !flat[ks].nil?
if flat[ks].is_a?(Array)
flat[ks] << v
else
flat[ks] = [flat[ks], v]
end
else
flat[ks] = v
end
end
flat
end
def flatten_to_array(value,a)
if value.is_a?(Array)
value.each {|e| flatten_to_array(e,a)}
elsif value.is_a?(Hash)
value.inject([]) {|aa,(k,v)| flatten_to_array(v,[]).each {|vv| aa << k+'/'+vv.to_s}; aa}.each {|e| a << e}
else
a << value.to_s
end
a
end
end
end
@@ -0,0 +1,161 @@
require 'hashery/crud_hash'
module Hashery
# A PropertyHash is the same as a regular Hash except it strictly limits the
# allowed keys.
#
# There are two ways to use it.
#
# 1) As an object in itself.
#
# h = PropertyHash.new(:a=>1, :b=>2)
# h[:a] #=> 1
# h[:a] = 3
# h[:a] #=> 3
#
# But if we try to set key that was not fixed, then we will get an error.
#
# h[:x] = 5 #=> ArgumentError
#
# 2) As a superclass.
#
# class MyPropertyHash < PropertyHash
# property :a, :default => 1
# property :b, :default => 2
# end
#
# h = MyPropertyHash.new
# h[:a] #=> 1
# h[:a] = 3
# h[:a] #=> 3
#
# Again, if we try to set key that was not fixed, then we will get an error.
#
# h[:x] = 5 #=> ArgumentError
#
class PropertyHash < CRUDHash
#
# Get a list of properties with default values.
#
# Returns [Hash] of properties and their default values.
#
def self.properties
@properties ||= (
parent = ancestors[1]
if parent.respond_to?(:properties)
parent.properties
else
{}
end
)
end
#
# Define a property.
#
# key - Name of property.
# opts - Property options.
# :default - Default value of property.
#
# Returns default value.
#
def self.property(key, opts={})
properties[key] = opts[:default]
end
#
# Initialize new instance of PropertyHash.
#
# properties - [Hash] Priming properties with default values, or
# if it doesn't respond to #each_pair, a default object.
# default_proc - [Proc] Procedure for default value of properties
# for properties without specific defaults.
#
def initialize(properties={}, &default_proc)
if properties.respond_to?(:each_pair)
super(&default_proc)
fixed = self.class.properties.merge(properties)
fixed.each_pair do |key, value|
store!(key, value)
end
else
super(*[properties].compact, &default_proc)
end
end
# Alias original #store method and make private.
alias :store! :store
private :store!
#
# Create a new property, on-the-fly.
#
# key - Name of property.
# opts - Property options.
# :default - Default value of property.
#
# Returns default value.
#
def property(key, opts={})
if opts[:default]
store!(key, opts[:default])
else
store!(key, retrieve(key))
end
end
#
# Store key value pair, ensuring the key is a valid property first.
#
# key - The `Object` to act as indexing key.
# value - The `Object` to associate with key.
#
# Raises ArgumentError if key is not a valid property.
#
# Returns +value+.
#
def store(key, value)
assert_key!(key)
super(key, value)
end
#
#def update(h)
# h.keys.each{ |k| assert_key!(k) }
# super(h)
#end
#
#def merge!(h)
# h.keys.each{ |k| assert_key!(k) }
# super(h)
#end
#
# Like #store but takes a two-element Array of `[key, value]`.
#
# Returns value.
#
#def <<(a)
# k,v = *a
# store(k,v)
#end
private
#
# Asserta that a key is a defined property.
#
# Raises ArgumentError if key is not a property.
#
def assert_key!(key)
unless key?(key)
raise ArgumentError, "property is not defined -- #{key.inspect}"
end
end
end
end
@@ -0,0 +1,91 @@
module Hashery
require 'hashery/key_hash'
# QueryHash is essentially a Hash class, but with some OpenStruct-like features.
#
# q = QueryHash.new
#
# Entries can be added to the Hash via a setter method.
#
# q.a = 1
#
# Then looked up via a query method.
#
# q.a? #=> 1
#
# The can also be looked up via a bang method.
#
# q.a! #=> 1
#
# The difference between query methods and bang methods is that the bang method
# will auto-instantiate the entry if not present, where as a query method will not.
#
# A QueryHash might not be quite as elegant as an OpenHash in that reader
# methods must end in `?` or `!`, but it remains fully compatible with Hash
# regardless of it's settings.
#
class QueryHash < CRUDHash
#
# By default the `key_proc` is set to convert all keys to strings via `#to_s`.
#
# default - Default object, or
# default_proc - Default procedure.
#
def initialize(*default, &default_proc)
@key_proc = Proc.new{ |k| k.to_s }
super(*default, &default_proc)
end
#
# Route get and set calls.
#
# s - [Symbol] Name of method.
# a - [Array] Method arguments.
# b - [Proc] Block argument.
#
# Examples
#
# o = QueryHash.new
# o.a = 1
# o.a? #=> 1
# o.b? #=> nil
#
def method_missing(s,*a, &b)
type = s.to_s[-1,1]
name = s.to_s.sub(/[!?=]$/, '')
key = name #key = cast_key(name)
case type
when '='
store(key, a.first)
when '!'
default = (default_proc ? default_proc.call(self, key) : default)
key?(key) ? fetch(key) : store(key, default)
when '?'
key?(key) ? fetch(key) : nil
else
# return self[key] if key?(key)
super(s,*a,&b)
end
end
#
# Custom #respond_to to account for #method_missing.
#
# name - The method name to check.
#
# Returns `true` or `false`.
#
def respond_to?(name)
return true if name.to_s.end_with?('=')
return true if name.to_s.end_with?('?')
return true if name.to_s.end_with?('!')
#key?(name.to_sym) || super(name)
super(name)
end
end
end
@@ -0,0 +1,14 @@
require 'hashery/key_hash'
module Hashery
# Stash is the original name for the KeyHash.
Stash = KeyHash
end
class Hash
# Convert Hash to Stash.
def to_stash
Hashery::Stash[self]
end
end
@@ -0,0 +1,53 @@
module Hashery
# StaticHash ia a Hash object which raises an error if any
# previously-defined key attempts to be set again.
#
# foo = StaticHash.new
# foo['name'] = 'Tom' #=> 'Tom'
# foo['age'] = 30 #=> 30
# foo['name'] = 'Bob'
#
# produces
#
# ArgumentError: Duplicate key for StaticHash -- 'name'
#
# StaticHash has it's orgins in Gavin Kistner's WriteOnceHash
# class found in his +basiclibrary.rb+ script.
#
# TODO: Maybe StaticHash isn't bets name for this class?
#
class StaticHash < CRUDHash
#
# Set a value for a key. Raises an error if that key already
# exists with a different value.
#
# key - Index key to associate with value.
# value - Value to associate with key.
#
# Retruns value.
#
def store(key, value)
if key?(key) && fetch(key) != value
raise ArgumentError, "Duplicate key for StaticHash -- #{key.inspect}"
end
super(key, value)
end
#
#def update(hash)
# dups = (keys | hash.keys)
# if dups.empty?
# super(hash)
# else
# raise ArgumentError, "Duplicate key for StaticHash -- #{dups.inspect}"
# end
#end
#
#alias_method :merge!, :update
end
end