public
Description: Case Insensitive Case Preserving Hash for ruby
Homepage: http://code.jeremyevans.net/doc/ruby-cicphash/
Clone URL: git://github.com/jeremyevans/ruby-cicphash.git
jeremyevans (author)
Sun Jul 06 15:07:50 -0700 2008
commit  82ba1d556e9d097abcd68470aed530dd85ff9107
tree    a3657d3c116047bbfc93f443cd0c7d3a108d07ae
parent  192aa613d3b032b413a469f74c9507c0714fcdd5
ruby-cicphash / cicphash.rb
100644 295 lines (255 sloc) 7.157 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# CICPHash is a case insensitive case preserving hash for ruby.
#
# * RDoc: http://cicphash.rubyforge.org
# * Source: http://github.com/jeremyevans/ruby-cicphash
# * Project: http://rubyforge.org/projects/cicphash/
#
# Copyright (c) 2007 Jeremy Evans
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
 
# Case Insensitive Case Preserving Hash
#
# CICPHash has the exact same interface as Hash, but is case insensitive
# and case preserving. Any value can be used as a key. However, you
# cannot have two keys in the same CICPHash that would be the same if when
# converted to strings would be equal or differing only in case.
#
# For example, all of the following keys would be considered equalivalent:
# 'ab', 'Ab', 'AB', 'aB', :ab, :Ab, :AB, :aB, ['A', :b], [nil, :A, nil, 'b'].
#
# CICPHash uses a last match wins policy. If an key-value pair is added to
# a CICPHash and a case insensitive variant of the key is already in the hash
# the instance of the key in the hash becomes the same the most recently added
# key (and obviously the value is updated to reflect the new value).
#
# You can change the rules determining which keys are equal by modifying the
# private convert_key method. By default, it is set to key.to_s.downcase.
# This method should produce the same output for any keys that you want to
# consider equal. For example, if you want :a and :A to be equal but :a to be
# different than "a", maybe key.inspect.downcase would work for you.
class CICPHash
  include Enumerable
  
  def self.[](*items)
    if items.length % 2 != 0
      if items.length == 1 && items.first.is_a?(Hash)
        new.merge(items.first)
      else
        raise ArgumentError, "odd number of arguments for CICPHash"
      end
    else
      hash = new
      loop do
        break if items.length == 0
        key = items.shift
        value = items.shift
        hash[key] = value
      end
      hash
    end
  end
  
  def initialize(*default, &block)
    if default.length > 1 || default.length == 1 && block_given?
      raise ArgumentError, "wrong number of arguments"
    end
    @name_hash = {}
    @hash = {}
    @default = default.first unless block_given?
    @default_proc = Proc.new(&block) if block_given?
  end
  
  def ==(hash)
    to_hash == hash.to_hash
  end
  
  def [](key)
    new_key = convert_key(key)
    if @hash.include?(new_key)
      @hash[new_key]
    elsif @default_proc
      @default_proc.call(self, key)
    else
      @default
    end
  end
 
  def []=(key, value)
    new_key = convert_key(key)
    @name_hash[new_key] = key
    @hash[new_key] = value
  end
  alias store :[]=
  
  def clear
    @name_hash.clear
    @hash.clear
  end
  
  def default
    @default
  end
  
  def default=(value)
    @default = value
  end
  
  def default_proc
    @default_proc
  end
  
  def delete(key)
    new_key = convert_key(key)
    @name_hash.delete(new_key)
    @hash.delete(new_key)
  end
  
  def delete_if(&block)
    hash = CICPHash.new
    each{|key, value| block.call(key, value) ? delete(key) : (hash[key] = value)}
    hash
  end
  
  def each
    @hash.each{|key, value| yield @name_hash[key], value }
  end
  alias each_pair each
  
  def each_key
    @hash.each_key{|key| yield @name_hash[key] }
  end
  
  def each_value
    @hash.each_value{|value| yield value }
  end
  
  def empty?
    @hash.empty?
  end
  
  def fetch(key, *default, &block)
    raise ArgumentError, "wrong number of arguments (#{default.length+1} for 2)" if default.length > 1
    if include?(key)
      self[key]
    elsif block_given?
      block.call(key)
    elsif default.length == 1
      default.first
    else
      raise IndexError, "key not found"
    end
  end
  
  def has_key?(key)
    @hash.has_key?(convert_key(key))
  end
  alias include? has_key?
  alias key? has_key?
  alias member? has_key?
  
  def has_value?(value)
    @hash.has_value?(value)
  end
  alias value? has_value?
  
  def index(value)
    @name_hash[@hash.index(value)]
  end
  
  def inspect
    to_hash.inspect
  end
  
  def invert
    hash = CICPHash.new
    each{|key, value| hash[value] = key}
    hash
  end
  
  def keys
    @name_hash.values
  end
  
  def length
    @hash.length
  end
  alias size length
  
  def merge(hash, &block)
    new_hash = CICPHash.new.merge!(self)
    hash.each do |key, value|
      new_hash[key] = if block_given? && new_hash.include?(key)
        block.call(key, new_hash[key], hash[key])
      else
        value
      end
    end
    new_hash
  end
  
  def rehash
    @name_hash.keys.each do |key|
      new_key = @name_hash[key].to_s.downcase
      if new_key != key
        @name_hash[new_key] = @name_hash.delete(key)
        @hash[new_key] = @hash.delete(key)
      end
    end
    self
  end
  
  def reject(&block)
    hash = CICPHash.new
    each{|key, value| hash[key] = self[key] unless block.call(key, value)}
    hash
  end
  
  def reject!(&block)
    hash = CICPHash.new
    changes = false
    each{|key, value| block.call(key, value) ? (changes = true; delete(key)) : (hash[key] = value)}
    changes ? hash : nil
  end
    
  def replace(hash)
    clear
    update(hash)
  end
  
  def select(&block)
    array = []
    each{|key, value| array << [key, value] if block.call(key, value)}
    array
  end
  
  def shift
    return nil if @name_hash.length == 0
    key, value = @name_hash.shift
    [value, @hash.delete(key)]
  end
  
  def sort(&block)
    block_given? ? to_a.sort(&block) : to_a.sort
  end
  
  def to_a
    array = []
    each{|key, value| array << [key, value]}
    array
  end
  
  def to_hash
    hash = {}
    each{|key, value| hash[key] = value}
    hash
  end
  
  def to_s
    to_a.join
  end
  
  def update(hash, &block)
    hash.each do |key, value|
      self[key] = if block_given? && include?(key)
        block.call(key, self[key], hash[key])
      else
        value
      end
    end
    self
  end
  alias merge! update
  
  def values
    @hash.values
  end
  
  def values_at(*keys)
    keys.collect{|key| self[key]}
  end
  alias indexes values_at
  alias indices values_at
  
  private
    def convert_key(key)
      key.to_s.downcase
    end
end