hipsterhash 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bdd5f38b32f9a671fb09d65b478f7f7ef334e286
4
+ data.tar.gz: 09a15c9f8c71ab5e9688fe8fae12e72ab86c9980
5
+ SHA512:
6
+ metadata.gz: 8e837e6299ccc72a60bcd331630c4962d5d5783e6a1461cadb12af9c10a20f285dc23bda9df89d58ca490e1b525b3380874e8287600435deec31cad293086821
7
+ data.tar.gz: fb2bf26f0fcfddc9e96ebc0a3b4ea226887e2285130ea2381e9d4148fec909a6a19816fb5339e65df80213ba9509dcae571a30b808f39ce398a6b88f7c23440f
data/README.md CHANGED
@@ -24,9 +24,6 @@ hh[:foo_bar] = "Pigs"
24
24
  hh["FooBar"] # => "Pigs"
25
25
  ```
26
26
 
27
- It uses [ActiveSupport][1] [Inflectors][2] internally, so you can probably muck
28
- around with them to modify the behaviour.
29
-
30
27
  ## Contributing
31
28
 
32
29
  1. Fork it
@@ -34,6 +31,3 @@ around with them to modify the behaviour.
34
31
  3. Commit your changes (`git commit -am 'Add some feature'`)
35
32
  4. Push to the branch (`git push origin my-new-feature`)
36
33
  5. Create new Pull Request
37
-
38
- [1]: https://github.com/rails/rails/tree/master/activesupport/lib/active_support
39
- [2]: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/inflections.rb
data/Rakefile CHANGED
@@ -9,4 +9,14 @@ Rake::TestTask.new do |t|
9
9
  t.test_files = FileList["test/**/*_test.rb"]
10
10
  end
11
11
 
12
+ desc "Run profiler to see what's slow"
13
+ task :profile do
14
+ require_relative './test/profile'
15
+ end
16
+
17
+ desc "Run benchmarks against Hash"
18
+ task :performance do
19
+ require_relative './test/performance'
20
+ end
21
+
12
22
  task :default => :test
data/hipsterhash.gemspec CHANGED
@@ -4,11 +4,14 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "hipsterhash"
7
- gem.version = "0.0.2"
8
- gem.authors = ["Bryan JJ Buckley"]
9
- gem.email = ["jjbuckley@gmail.com"]
10
- gem.description = %q{A hash which is all, like, whatever.}
11
- gem.summary = %q{A HipsterHash is just like a regular ruby Hash, except that it doesn't distinguish between symbols or strings, and the keys are case insensitive.}
7
+ gem.version = "0.0.3"
8
+ gem.authors = ["bjjb"]
9
+ gem.email = ["jj@bjjb.org"]
10
+ gem.summary = %q{A Hash which is all, like, whatever.}
11
+ gem.description = <<-DESC
12
+ A HipsterHash is just like a regular ruby Hash, except that it doesn't
13
+ distinguish between symbols or strings, and the keys are case insensitive.
14
+ DESC
12
15
  gem.homepage = "http://jjbuckley.github.com/hipsterhash"
13
16
 
14
17
  gem.files = `git ls-files`.split($/)
@@ -16,5 +19,6 @@ Gem::Specification.new do |gem|
16
19
  gem.test_files = gem.files.grep(%r{^(test)/})
17
20
  gem.require_paths = ["lib"]
18
21
 
19
- gem.add_dependency "activesupport"
22
+ gem.add_development_dependency "rake"
23
+ gem.add_development_dependency "minitest-focus"
20
24
  end
data/lib/hipster_hash.rb CHANGED
@@ -1,49 +1,66 @@
1
1
  # -*- encoding : utf-8 -*-
2
- require 'active_support/core_ext/string/inflections'
3
2
 
4
3
  # A HipsterHash is like a regular Hash, except the keys are semi case
5
4
  # insensitive, and symbols and strings are equivalent. Therefore, :foo_bar,
6
5
  # :FooBar, and "foo_bar" are considered to be the same key.
7
6
  class HipsterHash < Hash
8
- def initialize(initial = {}, &block)
9
- case initial
10
- when Hash
11
- super(&block)
12
- initial.each { |k, v| self[k] = v }
13
- else
14
- super
7
+
8
+ alias store! store
9
+
10
+ def store(key, value)
11
+ memoize(key) do
12
+ k, v = convert_key(key), convert_value(value)
13
+ store!(k, v)
15
14
  end
16
15
  end
17
16
 
17
+ def []=(key, value)
18
+ memoize(key) do
19
+ k, v = convert_key(key), convert_value(value)
20
+ store!(k, v)
21
+ end
22
+ end
23
+
24
+ def fetch(key)
25
+ super rescue super(convert_key(key)).tap { |v| store!(key, v) }
26
+ end
27
+
18
28
  def [](key)
19
- convert_value(super(convert_key(key)))
29
+ super || super(convert_key(key)).tap { |v| store!(key, v) }
20
30
  end
21
31
 
22
- def []=(key, value)
23
- super(convert_key(key), value)
32
+ def key?(sym)
33
+ super || super(convert_key(sym))
24
34
  end
25
35
 
26
- def method_missing(sym, *args, &block)
27
- if args.empty? and !block_given?
28
- self[sym] || super
29
- else
30
- super
31
- end
36
+ def self.[](*args)
37
+ new.tap { |hh| Hash[*args].each { |k, v| hh.store(k, v) } }
32
38
  end
33
39
 
34
40
  protected
35
- def convert_key(k)
36
- self.class.convert_key(k)
41
+
42
+ def method_missing(sym, *args, &block)
43
+ return super if block_given?
44
+ return store(sym[0...-1], args.first) if sym[-1] == '=' and args.length == 1
45
+ return fetch(sym) if args.empty? and key?(sym)
46
+ super
37
47
  end
38
48
 
39
- def self.convert_key(k)
40
- k.to_s.underscore.to_sym
49
+ def converted_keys
50
+ @converted_keys ||= Hash.new do |h, k|
51
+ h[k] = underscore(k.to_s).to_sym
52
+ end
53
+ end
54
+
55
+ def convert_key(k)
56
+ converted_keys[k]
41
57
  end
42
58
 
43
59
  def self.convert_value(arg)
44
60
  case arg
45
- when Hash then new(arg)
46
- when Array then arg.map { |a| convert_value(a) }
61
+ when self then arg
62
+ when Hash then self[arg]
63
+ when Array then arg.map { |x| convert_value(x) }
47
64
  else arg
48
65
  end
49
66
  end
@@ -51,4 +68,20 @@ protected
51
68
  def convert_value(arg)
52
69
  self.class.convert_value(arg)
53
70
  end
71
+
72
+ def underscore(s)
73
+ s.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z])([A-Z])/, '\1_\2').downcase
74
+ end
75
+
76
+ def memoize(sym, &block)
77
+ yield.tap do |x|
78
+ eigenklass.class_eval { define_method(:"#{sym}") { x } } unless @@unmemoizable.key?(convert_key(sym))
79
+ end
80
+ end
81
+
82
+ def eigenklass
83
+ @eigenklass ||= (class << self; self; end)
84
+ end
85
+
86
+ @@unmemoizable = Hash[[methods + instance_methods].flatten.uniq.zip([])]
54
87
  end
data/lib/hipsterhash.rb CHANGED
@@ -1,2 +1,2 @@
1
1
  # -*- encoding : utf-8 -*-
2
- require 'hipster_hash'
2
+ require_relative './hipster_hash'
@@ -1,51 +1,139 @@
1
1
  # -*- encoding : utf-8 -*-
2
- require 'test/unit'
3
- require File.expand_path('../../lib/hipster_hash', __FILE__)
2
+ require 'test_helper'
3
+ require 'hipster_hash'
4
4
 
5
- class HipsterHashTest < Test::Unit::TestCase
6
- def test_normal_behaviour
5
+ describe HipsterHash do
6
+ it "acts like a hash" do
7
7
  hh = HipsterHash.new
8
- hh["foo"] = "bar"
9
- assert_kind_of Hash, hh
10
- assert_equal "bar", hh[:foo]
11
- assert_nil hh[:pigs]
8
+ hh.must_be_kind_of Hash
9
+ hh.store(:foo, :bar)
10
+ hh.fetch(:foo).must_equal :bar
11
+ hh[:bar] = 123
12
+ hh[:bar].must_equal 123
13
+ hh.keys.must_equal [:foo, :bar]
14
+ hh.delete(:foo).must_equal :bar
15
+ hh.keys.must_equal [:bar]
16
+ lambda { hh.fetch(:foo) }.must_raise KeyError
17
+ lambda { hh.fetch('foo') }.must_raise KeyError
18
+ lambda { hh.fetch('Foo') }.must_raise KeyError
19
+ hh[:foo].must_be_nil
20
+ hh = HipsterHash[:foo, 123, :bar, 234]
21
+ hh[:foo].must_equal 123
22
+ hh[:bar].must_equal 234
23
+ hh.fetch(:foo).must_equal 123
24
+ hh.fetch('foo').must_equal 123
25
+ hh.fetch('Foo').must_equal 123
26
+ hh = HipsterHash[[[:foo, 123], [:bar, 234]]]
27
+ hh[:foo].must_equal 123
28
+ hh[:bar].must_equal 234
29
+ hh = HipsterHash[{foo: 123, bar: 234}]
30
+ hh[:foo].must_equal 123
31
+ hh[:bar].must_equal 234
32
+ hh[:bar] = 77
33
+ hh[:bar].must_equal 77
12
34
  end
13
35
 
14
- def test_indifference_to_input_type
15
- hh = HipsterHash.new(:foo => "bar")
16
- assert_equal "bar", hh["foo"]
17
- hh['foo'] = "baz"
18
- assert_equal "baz", hh[:foo]
36
+ it "treats symbols and strings equally" do
37
+ hh = HipsterHash[foo: "bar"]
38
+ hh.keys.must_equal [:foo]
39
+ hh.key?(:foo).must_equal true
40
+ hh.foo.must_equal "bar"
41
+ hh['foo'] = 'baz'
42
+ hh.keys.must_equal [:foo]
43
+ hh[:foo].must_equal "baz"
19
44
  end
20
45
 
21
- def test_nesting
22
- hh = HipsterHash.new(:foo => { :bar => :baz })
23
- assert_kind_of HipsterHash, hh[:foo]
24
- assert_equal :baz, hh["Foo"][:Bar]
46
+ it "handles nested hashes" do
47
+ hh = HipsterHash[foo: { bar: :baz }]
48
+ hh.foo.must_be_kind_of HipsterHash
49
+ hh["Foo"][:Bar].must_equal :baz
25
50
  end
26
51
 
27
- def test_case_indifference
52
+ it "handles nested arrays" do
53
+ hh = HipsterHash[{foo: { bar: [ { baz: 1 } ] }}]
54
+ hh.foo.must_be_kind_of HipsterHash
55
+ hh.foo.bar[0].baz.must_equal 1
56
+ hh = HipsterHash[{"foo" => { "bar" => [ { "baz" => 1 } ] }}]
57
+ hh.foo.bar[0].baz.must_equal 1
58
+ end
59
+
60
+ it "is indifferent to case" do
28
61
  hh = HipsterHash.new
29
62
  hh[:foo_bar] = "bar"
30
- assert_equal "bar", hh["FooBar"]
63
+ hh["FooBar"].must_equal "bar"
31
64
  hh['FooBar'] = "baz"
32
- assert_equal "baz", hh[:foo_bar]
65
+ hh[:foo_bar].must_equal "baz"
33
66
  end
34
67
 
35
- def test_creation_with_a_hash
36
- hh = HipsterHash.new(:foo_bar => "Bar")
37
- assert_equal "Bar", hh["FooBar"]
68
+ it "can be inherited" do
69
+ hc = Class.new(HipsterHash)
70
+ hh = hc[foo_bar: "Baz"]
71
+ hh[:foo_bar].must_equal "Baz"
72
+ hh[:FooBar].must_equal "Baz"
73
+ hh["FooBar"].must_equal "Baz"
74
+ hh["foo_bar"].must_equal "Baz"
75
+ hh.foo_bar.must_equal "Baz"
38
76
  end
39
77
 
40
- def test_subclassing
41
- hc = Class.new(HipsterHash)
42
- hh = hc.new(:foo_bar => "Baz")
43
- assert_equal "Baz", hh["FooBar"]
78
+ it "acts like an openstruct" do
79
+ hh = HipsterHash[:foo, 123]
80
+ hh.bar = 234
81
+ hh.foo.must_equal 123
82
+ hh.bar.must_equal 234
83
+ lambda { hh.baz }.must_raise NoMethodError
84
+ hh[:foo] = "bar"
85
+ hh.foo.must_equal "bar"
86
+ hh.foo = "baz"
87
+ hh.foo.must_equal "baz"
88
+ hh.store("foo", "bpb")
89
+ hh.foo.must_equal "bpb"
90
+ hh2 = HipsterHash[:foo, 999]
91
+ hh2.foo.must_equal 999
92
+ hh.foo.must_equal "bpb"
93
+ end
94
+
95
+ it "doesn't break if you use a special method name" do
96
+ hh = HipsterHash.new
97
+ hh[:foo] = 1
98
+ hh.foo.must_equal 1
99
+ m = hh.method(:[])
100
+ hh['[]'] = "blah"
101
+ hh.method(:[]).must_equal m
102
+ hh.foo.must_equal 1
103
+ hh['[]'].must_equal "blah"
104
+ hh.eigenklass = "blah"
105
+ hh.foo.must_equal 1
106
+ hh.foo = 2
107
+ hh.foo.must_equal 2
108
+ end
109
+
110
+ it "doesn't break with funny keys" do
111
+ hh = HipsterHash.new
112
+ hh[0] = 1
113
+ hh[0].must_equal 1
114
+ hh[":blah"] = 99
115
+ hh.send(":blah").must_equal 99
116
+ hh[nil] = "klm"
117
+ hh[nil].must_equal "klm"
118
+ hh[:'→'] = "arrow"
119
+ hh.send('→').must_equal 'arrow'
44
120
  end
45
121
 
46
- def test_method_missing
47
- hh = HipsterHash.new(:Foo => :bar)
48
- assert_equal :bar, hh.foo
49
- assert_equal :bar, hh.FOO
122
+ describe "key conversion" do
123
+ it "leaves underscored symbols alone" do
124
+ convert(:foo).must_equal :foo
125
+ end
126
+ it "converts underscored strings to symbols" do
127
+ convert("foo").must_equal :foo
128
+ end
129
+ it "underscores camelcase symbols" do
130
+ convert(:FooBar).must_equal :foo_bar
131
+ end
132
+ it "underscores and symbolizes camelcased strings" do
133
+ convert("FooBar").must_equal :foo_bar
134
+ end
135
+ def convert(k)
136
+ HipsterHash.new.send(:convert_key, k)
137
+ end
50
138
  end
51
139
  end
@@ -0,0 +1,16 @@
1
+ #! /usr/bin/env ruby -w
2
+ require 'benchmark'
3
+ require_relative '../lib/hipsterhash'
4
+
5
+ h = { foo: { bar: { x: "123", y: [ 1, 2, 3 ] } }, baz: "123" }
6
+ hh = HipsterHash[h.dup]
7
+
8
+ n = 1000000
9
+ Benchmark.bmbm do |x|
10
+ x.report("Hash#[] ") { n.times { h[:foo][:bar][:x] } }
11
+ x.report("HipsterHash#[] ") { n.times { hh[:foo][:bar][:x] } }
12
+ x.report("Hash#fetch ") { n.times { h.fetch(:foo).fetch(:bar).fetch(:x) } }
13
+ x.report("HipsterHash#fetch ") { n.times { hh.fetch(:foo).fetch(:bar).fetch(:x) } }
14
+ x.report("HipsterHash#fetch2 ") { n.times { hh.fetch(:"Foo").fetch(:"Bar").fetch(:"X") } }
15
+ x.report("HipsterHash#method_missing") { n.times { hh.foo.bar.x } }
16
+ end
data/test/profile.rb ADDED
@@ -0,0 +1,12 @@
1
+ #! /usr/bin/env ruby -w
2
+
3
+ require_relative '../lib/hipsterhash'
4
+
5
+
6
+ require 'profile'
7
+ hh = HipsterHash[foo: { bar: { x: "123" } }]
8
+
9
+ n = 1000
10
+ n.times do
11
+ hh.foo["Bar"].x
12
+ end
@@ -0,0 +1,2 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/focus'
metadata CHANGED
@@ -1,40 +1,53 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hipsterhash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
5
- prerelease:
4
+ version: 0.0.3
6
5
  platform: ruby
7
6
  authors:
8
- - Bryan JJ Buckley
7
+ - bjjb
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-10-22 00:00:00.000000000 Z
11
+ date: 2014-03-29 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
- name: activesupport
14
+ name: rake
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - ">="
20
18
  - !ruby/object:Gem::Version
21
19
  version: '0'
22
- type: :runtime
20
+ type: :development
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ! '>='
24
+ - - ">="
28
25
  - !ruby/object:Gem::Version
29
26
  version: '0'
30
- description: A hash which is all, like, whatever.
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest-focus
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: |
42
+ A HipsterHash is just like a regular ruby Hash, except that it doesn't
43
+ distinguish between symbols or strings, and the keys are case insensitive.
31
44
  email:
32
- - jjbuckley@gmail.com
45
+ - jj@bjjb.org
33
46
  executables: []
34
47
  extensions: []
35
48
  extra_rdoc_files: []
36
49
  files:
37
- - .gitignore
50
+ - ".gitignore"
38
51
  - Gemfile
39
52
  - LICENSE.txt
40
53
  - README.md
@@ -43,30 +56,34 @@ files:
43
56
  - lib/hipster_hash.rb
44
57
  - lib/hipsterhash.rb
45
58
  - test/hipster_hash_test.rb
59
+ - test/performance.rb
60
+ - test/profile.rb
61
+ - test/test_helper.rb
46
62
  homepage: http://jjbuckley.github.com/hipsterhash
47
63
  licenses: []
64
+ metadata: {}
48
65
  post_install_message:
49
66
  rdoc_options: []
50
67
  require_paths:
51
68
  - lib
52
69
  required_ruby_version: !ruby/object:Gem::Requirement
53
- none: false
54
70
  requirements:
55
- - - ! '>='
71
+ - - ">="
56
72
  - !ruby/object:Gem::Version
57
73
  version: '0'
58
74
  required_rubygems_version: !ruby/object:Gem::Requirement
59
- none: false
60
75
  requirements:
61
- - - ! '>='
76
+ - - ">="
62
77
  - !ruby/object:Gem::Version
63
78
  version: '0'
64
79
  requirements: []
65
80
  rubyforge_project:
66
- rubygems_version: 1.8.24
81
+ rubygems_version: 2.2.0
67
82
  signing_key:
68
- specification_version: 3
69
- summary: A HipsterHash is just like a regular ruby Hash, except that it doesn't distinguish
70
- between symbols or strings, and the keys are case insensitive.
83
+ specification_version: 4
84
+ summary: A Hash which is all, like, whatever.
71
85
  test_files:
72
86
  - test/hipster_hash_test.rb
87
+ - test/performance.rb
88
+ - test/profile.rb
89
+ - test/test_helper.rb