This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
module Hashery
|
||||
VERSION = "2.1.0"
|
||||
end
|
||||
|
||||
require 'hashery/core_ext'
|
||||
|
||||
#require 'hashery/basic_struct'
|
||||
require 'hashery/casting_hash'
|
||||
require 'hashery/crud_hash'
|
||||
require 'hashery/dictionary'
|
||||
require 'hashery/fuzzy_hash'
|
||||
require 'hashery/ini_hash'
|
||||
require 'hashery/linked_list'
|
||||
require 'hashery/lru_hash'
|
||||
require 'hashery/key_hash'
|
||||
require 'hashery/open_cascade'
|
||||
require 'hashery/open_hash'
|
||||
#require 'hashery/ordered_hash'
|
||||
#require 'hashery/ostructable'
|
||||
require 'hashery/property_hash'
|
||||
require 'hashery/query_hash'
|
||||
require 'hashery/static_hash'
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
revision: 2013
|
||||
type: ruby
|
||||
sources:
|
||||
- Index.yml
|
||||
- Gemfile
|
||||
authors:
|
||||
- name: Trans
|
||||
email: transfire@gmail.com
|
||||
- name: Kirk Haines
|
||||
- name: Robert Klemme
|
||||
- name: Jan Molic
|
||||
- name: George Moschovitis
|
||||
- name: Jeena Paradies
|
||||
- name: Erik Veenstra
|
||||
organizations:
|
||||
- name: RubyWorks (http://rubyworks.github.com/)
|
||||
requirements:
|
||||
- groups:
|
||||
- development
|
||||
- test
|
||||
version: ">= 0"
|
||||
name: qed
|
||||
- groups:
|
||||
- development
|
||||
- test
|
||||
version: ">= 0"
|
||||
name: lemon
|
||||
- groups:
|
||||
- development
|
||||
- test
|
||||
version: ">= 0"
|
||||
name: rubytest-cli
|
||||
conflicts: []
|
||||
alternatives: []
|
||||
resources:
|
||||
- type: home
|
||||
uri: http://rubyworks.github.com/hashery
|
||||
label: Homepage
|
||||
- type: code
|
||||
uri: http://github.com/rubyworks/hashery
|
||||
label: Source Code
|
||||
- type: mail
|
||||
uri: http://groups.google.com/group/rubyworks-mailinglist
|
||||
label: Mailing List
|
||||
- type: docs
|
||||
uri: http://rubydoc.info/github/rubyworks/hashery/master/frames
|
||||
label: Documentation
|
||||
- type: wiki
|
||||
uri: http://wiki.github.com/rubyworks/hashery
|
||||
label: User Guide
|
||||
- type: gems
|
||||
uri: http://rubygems.org/gems/hashery
|
||||
repositories:
|
||||
- name: upstream
|
||||
scm: git
|
||||
uri: git://github.com/rubyworks/hashery.git
|
||||
categories: []
|
||||
copyrights:
|
||||
- holder: Rubyworks
|
||||
year: '2010'
|
||||
license: BSD-2-Clause
|
||||
customs: []
|
||||
paths:
|
||||
lib:
|
||||
- lib
|
||||
- alt
|
||||
name: hashery
|
||||
title: Hashery
|
||||
version: 2.1.2
|
||||
summary: Facets-bread collection of Hash-like classes.
|
||||
description: The Hashery is a tight collection of Hash-like classes. Included among
|
||||
its many offerings are the auto-sorting Dictionary class, the efficient LRUHash,
|
||||
the flexible OpenHash and the convenient KeyHash. Nearly every class is a subclass
|
||||
of the CRUDHash which defines a CRUD model on top of Ruby's standard Hash making
|
||||
it a snap to subclass and augment to fit any specific use case.
|
||||
created: '2010-04-21'
|
||||
date: '2016-05-01'
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user