speculation 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b293a72669d39d6366f5c1f42a3112fdfa03e6c5
4
- data.tar.gz: a90bc4265285e116cb93978928546a0f9f18256e
3
+ metadata.gz: 19dd9abfde4552ea62d19dda2714f9dd0e6f33a8
4
+ data.tar.gz: 112bed767778c508470b5d3303937580158bd649
5
5
  SHA512:
6
- metadata.gz: 7c0f3352a89d81340eaf8ddd583b8d5f2dcaeb68a1da4d025f4af680a0edc5aba522cfeb997152484753ee072217efd3bb64877e297937e03af16852779f751e
7
- data.tar.gz: d4a90effba95011da873b7aa3c03d496a59379dd7d890c0aaf5e3101a0ec2a1e8ef9840710c13e257f10ecbef7144457679b47e7a14acafd8bb92a8835f6e572
6
+ metadata.gz: 1e0d5b36cb7c3e868004e4e62b408bc1a80c144759791c2ae138fefcef90a345106fde0780c7ac8b1fee36c0049c3e8f922af8a72e9ceeed9c133337d494514f
7
+ data.tar.gz: fb3eb18c1a8a4e1be72a0223f30c11dd05170a1b9225bfd73bebb6a2f27bda4255d51ae952597bf5bf3dd8301ce6990cac1fbe481d3897caf9f54e5b531cc7ef
data/.rubocop.yml CHANGED
@@ -3,6 +3,7 @@ AllCops:
3
3
  - 'bin/*'
4
4
  - '*.*'
5
5
  - 'vendor/**/*'
6
+ - 'examples/**/*'
6
7
  TargetRubyVersion: 2.4
7
8
 
8
9
  Metrics/ClassLength:
@@ -85,3 +86,15 @@ Style/NumericLiterals:
85
86
 
86
87
  Style/NumericPredicate:
87
88
  Enabled: false
89
+
90
+ Style/SpecialGlobalVars:
91
+ Enabled: false
92
+
93
+ Style/RescueModifier:
94
+ Enabled: false
95
+
96
+ Performance/RedundantBlockCall:
97
+ Enabled: false
98
+
99
+ Style/MethodName:
100
+ Enabled: false
data/.travis.yml CHANGED
@@ -2,6 +2,8 @@ sudo: false
2
2
  language: ruby
3
3
  cache: bundler
4
4
  rvm:
5
+ - 2.0.0
6
+ - 2.1.10
5
7
  - 2.2.6
6
8
  - 2.3.3
7
9
  - 2.4.0
@@ -10,7 +12,3 @@ rvm:
10
12
  before_install:
11
13
  - gem update --system
12
14
  - gem install bundler
13
- matrix:
14
- allow_failures:
15
- - rvm: 2.2.6
16
- - rvm: jruby-9.0.5.0
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Speculation
1
+ # Speculation [![Build Status](https://travis-ci.org/english/speculation.svg?branch=master)](https://travis-ci.org/english/speculation)
2
2
 
3
- A Ruby port of Clojure's `clojure.spec`. See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec). The `Speculation` library is largely a copy-and-paste from `clojure.spec`. All credit goes to the clojure.spec authors.
3
+ A Ruby port of Clojure's `clojure.spec`. See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec). All advantages/disadvantages for clojure.spec should apply to Speculation too. This library is largely a copy-and-paste from clojure.spec so all credit goes to the clojure.spec authors.
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,9 +18,18 @@ Or install it yourself as:
18
18
 
19
19
  $ gem install speculation
20
20
 
21
+ ## Project Goals
22
+
23
+ The goal of this project is to match clojure.spec as closely as possible, from design to features to API. This decision comes with the trade-off that the library may not necessarily be idiomatic Ruby, however there's nothing stopping other libraries from being built atop Speculation to bring a more Ruby-like feel. This library won't introduce features that do not exist in clojure.spec.
24
+
25
+ ## Examples
26
+
27
+ - [spec_guide.rb](examples/spec_guide.rb): Speculation port of Clojure's [spec guide](https://clojure.org/guides/spec)
28
+ - [codebreaker.rb](examples/codebreaker.rb): Speculation port of the 'codebreaker' game described in [Interactive development with clojure.spec](http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec)
29
+
21
30
  ## Usage
22
31
 
23
- The API is more-or-less the same as `clojure.spec`. If you're already familiar with `clojure.spec` then you should be write at home with `Speculation`. Clojure and Ruby and quite different languages, so naturally there are some differences:
32
+ The API is more-or-less the same as `clojure.spec`. If you're already familiar with then you should be write at home with Speculation. Clojure and Ruby and quite different languages, so naturally there are some differences:
24
33
 
25
34
  ### Built in predicates
26
35
 
@@ -35,25 +44,25 @@ S.valid?(/^\d+$/, "123")
35
44
  S.valid?(Set[:foo, :bar, :baz], :foo)
36
45
  ```
37
46
 
38
- ### Namespaced keywords
47
+ ### Namespaced keywords/symbols
39
48
 
40
49
  Namespaced keywords are at the core of `clojure.spec`. Since spec utilises a global spec registry, namespaced keywords allow libraries to register specs with the same names but under different namespaces, thus removing accidental collisions. Ruby's equivalent to Clojure's keywords are Symbols. Ruby Symbol's don't have namespaces.
41
50
 
42
- In order keep the global spec registry architecture in Speculation, we utilise refinements achieve similar behaviour:
51
+ In order keep the global spec registry architecture in Speculation, we utilise a helper method `ns` achieve similar behaviour:
43
52
 
44
53
  ```rb
45
- using Speculation::NamespacedSymbols.refine(MyModule)
54
+ extend Speculation::NamespacedSymbols
46
55
 
47
- p :foo.ns
56
+ p ns(:foo)
48
57
  # => :"MyModule/foo"
49
58
 
50
- p :foo.ns(AnotherModule)
59
+ p ns(AnotherModule, :foo)
51
60
  # => :"AnotherModule/foo"
52
61
  ```
53
62
 
54
63
  ### FSpecs
55
64
 
56
- #### Symbols
65
+ #### Symbols/Methods
57
66
 
58
67
  Clojure uses Symbols to refer to functions. To refer to a method in Ruby, we must use the `method` method.
59
68
 
@@ -96,8 +105,6 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/englis
96
105
  ## TODO
97
106
 
98
107
  - tidy up tests
99
- - write up comparison with clojure.spec
100
- - write guide
101
108
 
102
109
  ### clojure.spec features
103
110
 
@@ -108,7 +115,10 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/englis
108
115
 
109
116
  ### Improvements
110
117
 
111
- - Find/build an alternative to Rantly
118
+ - Explore alternative generator library
119
+ - Build up a library of generators around Rantly in the meantime?
120
+ - Generate documentation from specs
121
+ - perhaps integrating with [Pry's documentation browsing](https://github.com/pry/pry/wiki/Documentation-browsing)?
112
122
  - Profile and optimise
113
123
 
114
124
  ## License
data/Rakefile CHANGED
@@ -9,17 +9,13 @@ task :rubocop do
9
9
  end
10
10
 
11
11
  task :test do
12
- if RUBY_PLATFORM == "java"
13
- $LOAD_PATH.unshift(File.expand_path("../test", __FILE__))
12
+ $LOAD_PATH.unshift(File.expand_path("../test", __FILE__))
14
13
 
15
- FileList["test/**/*.rb"].each do |test_file|
16
- require "./#{test_file}"
17
- end
18
-
19
- raise unless Minitest.run
20
- else
21
- sh "TEST_QUEUE_SPLIT_GROUPS=1 bundle exec ruby -r minitest/autorun -I test -S minitest-queue $(find test -name *_test.rb)"
14
+ FileList["test/**/*.rb"].each do |test_file|
15
+ require "./#{test_file}"
22
16
  end
17
+
18
+ raise unless Minitest.run
23
19
  end
24
20
 
25
21
  task :doc do
data/bin/console CHANGED
@@ -3,11 +3,11 @@
3
3
  $LOAD_PATH.unshift File.expand_path("../../test", __FILE__)
4
4
 
5
5
  require "bundler/setup"
6
+ require "pry"
6
7
  require 'rubocop'
7
8
  require "speculation"
8
9
  require "speculation/test"
9
10
  require "speculation/gen"
10
- require "speculation/utils_specs"
11
11
 
12
12
  def reload!
13
13
  Minitest::Test.reset
@@ -18,7 +18,6 @@ def reload!
18
18
  end
19
19
 
20
20
  load "./lib/speculation/namespaced_symbols.rb"
21
- load "./lib/speculation/conj.rb"
22
21
  load "./lib/speculation/pmap.rb"
23
22
  load "./lib/speculation/identifier.rb"
24
23
  load "./lib/speculation/utils.rb"
@@ -33,7 +32,6 @@ def reload!
33
32
  load "./lib/speculation.rb"
34
33
  load "./lib/speculation/gen.rb"
35
34
  load "./lib/speculation/test.rb"
36
- load "./lib/speculation/utils_specs.rb"
37
35
 
38
36
  Dir["test/**/*.rb"].each do |f|
39
37
  load f
@@ -44,7 +42,7 @@ def reload!
44
42
  Object.const_set(:Gen, Speculation::Gen)
45
43
  Object.const_set(:U, Speculation::Utils)
46
44
 
47
- # STest.instrument
45
+ STest.instrument
48
46
  end
49
47
 
50
48
  def t
@@ -61,10 +59,59 @@ require "minitest"
61
59
  include Minitest::Assertions
62
60
  def self.assertions; 1; end
63
61
  def self.assertions=(_x); end
64
- using Speculation::NamespacedSymbols.refine(self)
62
+
63
+ extend Speculation::NamespacedSymbols
64
+
65
+ def self.test_check_utils
66
+ S.fdef(U.method(:hash?),
67
+ :args => S.tuple(ns(S, :any)),
68
+ :ret => ns(S, :boolean))
69
+
70
+ S.fdef(U.method(:array?),
71
+ :args => S.tuple(ns(S, :any)),
72
+ :ret => ns(S, :boolean))
73
+
74
+ S.fdef(U.method(:collection?),
75
+ :args => S.tuple(ns(S, :any)),
76
+ :ret => ns(S, :boolean))
77
+
78
+ S.fdef(U.method(:itself),
79
+ :args => S.cat(:x => ns(S, :any)),
80
+ :ret => ns(S, :any),
81
+ :fn => ->(x) { x[:args][:x].equal?(x[:ret]) })
82
+
83
+ # S.fdef(U.method(:complement),
84
+ # :args => ns(S, :empty),
85
+ # :block => S.fspec(:args => S.zero_or_more(ns(S, :any)),
86
+ # :ret => ns(S, :any)),
87
+ # :ret => S.fspec(:args => S.zero_or_more(ns(S, :any)),
88
+ # :ret => ns(S, :boolean)))
89
+
90
+ S.fdef(U.method(:constantly),
91
+ :args => S.cat(:x => ns(S, :any)),
92
+ :ret => Proc,
93
+ :fn => ->(x) { x[:args][:x].equal?(x[:ret].call) })
94
+
95
+ S.fdef(U.method(:distinct?),
96
+ :args => S.cat(:coll => Enumerable),
97
+ :ret => ns(S, :boolean))
98
+
99
+ S.fdef(U.method(:ident?),
100
+ :args => S.cat(:x => ns(S, :any)),
101
+ :ret => ns(S, :boolean))
102
+
103
+ S.fdef(U.method(:method?),
104
+ :args => S.cat(:x => ns(S, :any)),
105
+ :ret => ns(S, :boolean))
106
+
107
+ S.fdef(U.method(:empty),
108
+ :args => S.cat(:coll => Enumerable),
109
+ :ret => S.and(Enumerable, ->(coll) { coll.empty? }),
110
+ :fn => ->(x) { x[:args][:coll].class == x[:ret].class })
111
+
112
+ STest.summarize_results(STest.check(STest.enumerate_methods(Speculation::Utils), :num_tests => 50))
113
+ end
65
114
 
66
115
  reload!
67
116
 
68
- # (If you use this, don't forget to add pry to your Gemfile!)
69
- require "pry"
70
117
  Pry.start
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+ # See http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec
3
+
4
+ require "bundler/inline"
5
+ require "set"
6
+
7
+ gemfile true do
8
+ gem "speculation",
9
+ :git => "https://github.com/english/speculation.git",
10
+ :require => ["speculation", "speculation/test", "speculation/gen"]
11
+ end
12
+
13
+ S = Speculation
14
+ Gen = S::Gen
15
+ STest = S::Test
16
+
17
+ extend Speculation::NamespacedSymbols
18
+
19
+ peg = Set[:y, :g, :r, :c, :w, :b]
20
+
21
+ S.def ns(:code), S.coll_of(peg, :min_count => 4, :max_count => 6)
22
+
23
+ def score; end
24
+
25
+ S.fdef method(:score),
26
+ :args => S.cat(:secret => ns(:code), :guess => ns(:code))
27
+
28
+ S.exercise S.get_spec(method(:score)).args
29
+
30
+ # [[[[:c, :g, :y, :w, :g, :y], [:y, :y, :c, :r, :y, :y]], {:secret=>[:c, :g, :y, :w, :g, :y], :guess=>[:y, :y, :c, :r, :y, :y]}],
31
+ # [[[:b, :b, :y, :y], [:b, :r, :y, :y, :y]], {:secret=>[:b, :b, :y, :y], :guess=>[:b, :r, :y, :y, :y]}],
32
+ # [[[:y, :y, :w, :g, :r, :w], [:r, :w, :c, :b, :r, :y]], {:secret=>[:y, :y, :w, :g, :r, :w], :guess=>[:r, :w, :c, :b, :r, :y]}],
33
+ # ...
34
+
35
+ S.fdef method(:score),
36
+ :args => S.and(S.cat(:secret => ns(:code), :guess => ns(:code)),
37
+ ->(args) { args[:secret].count == args[:guess].count })
38
+
39
+ S.exercise S.get_spec(method(:score)).args
40
+
41
+ # [[[[:b, :w, :b, :c, :b], [:y, :r, :y, :y, :b]], {:secret=>[:b, :w, :b, :c, :b], :guess=>[:y, :r, :y, :y, :b]}],
42
+ # [[[:c, :y, :w, :c], [:c, :r, :c, :c]], {:secret=>[:c, :y, :w, :c], :guess=>[:c, :r, :c, :c]}],
43
+ # [[[:g, :g, :y, :g], [:b, :y, :w, :b]], {:secret=>[:g, :g, :y, :g], :guess=>[:b, :y, :w, :b]}],
44
+ # ...
45
+
46
+ S.def ns(:exact_matches), ns(S, :natural_integer)
47
+ S.def ns(:loose_matches), ns(S, :natural_integer)
48
+
49
+ S.fdef method(:score),
50
+ :args => S.and(S.cat(:secret => ns(:code), :guess => ns(:code)),
51
+ ->(args) { args[:secret].count == args[:guess].count }),
52
+ :ret => S.keys(:req => [ns(:exact_matches), ns(:loose_matches)])
53
+
54
+ S.exercise S.get_spec(method(:score)).ret
55
+ # [[{:"main/exact_matches"=>301501626008109845, :"main/loose_matches"=>1592567845535536138},
56
+ # {:"main/exact_matches"=>301501626008109845, :"main/loose_matches"=>1592567845535536138}],
57
+ # [{:"main/exact_matches"=>705260057025726755, :"main/loose_matches"=>4979282811122408}, {:"main/exact_matches"=>705260057025726755, :"main/loose_matches"=>4979282811122408}],
58
+ # [{:"main/exact_matches"=>905565787875512744, :"main/loose_matches"=>965211463791348650},
59
+ # ...
60
+
61
+ S.fdef method(:score),
62
+ :args => S.and(S.cat(:secret => ns(:code), :guess => ns(:code)),
63
+ ->(args) { args[:secret].count == args[:guess].count }),
64
+ :ret => S.keys(:req => [ns(:exact_matches), ns(:loose_matches)]),
65
+ :fn => ->(fn) {
66
+ sum_matches = fn[:ret].values.reduce(&:+)
67
+ sum_matches.between?(0, fn[:args][:secret].count)
68
+ }
69
+
70
+ def self.score(secret, guess)
71
+ { ns(:exact_matches) => 0,
72
+ ns(:loose_matches) => 0 }
73
+ end
74
+
75
+ STest.check method(:score)
76
+ # [{:spec=>Speculation::FSpec(main.score), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
77
+
78
+ def self.score(secret, guess)
79
+ { ns(:exact_matches) => 4,
80
+ ns(:loose_matches) => 3 }
81
+ end
82
+
83
+ # STest.check method(:score)
84
+
85
+ # [{:spec=>Speculation::FSpec(main.score),
86
+ # :"Speculation::Test/ret"=>
87
+ # {:fail=>[[:y, :b, :r, :r, :b], [:y, :w, :g, :r, :g]],
88
+ # :block=>nil,
89
+ # :num_tests=>1,
90
+ # :result=>
91
+ # #<Speculation::Error: {:"Speculation/problems"=>
92
+ # [{:path=>[:fn],
93
+ # :val=>
94
+ # {:args=>{:secret=>[:y, :b, :r, :r, :b], :guess=>[:y, :w, :g, :r, :g]},
95
+ # :block=>nil,
96
+ # :ret=>{:"main/exact_matches"=>4, :"main/loose_matches"=>3}},
97
+ # :via=>[],
98
+
99
+ def self.score(secret, guess)
100
+ { ns(:exact_matches) => secret.zip(guess).count { |(a, b)| a.equal?(b) },
101
+ ns(:loose_matches) => 0 }
102
+ end
103
+
104
+ S.exercise_fn method(:score)
105
+
106
+ # [[[[:c, :w, :r, :c, :c, :b], [:c, :y, :y, :r, :c, :b]], {:"main/exact_matches"=>3, :"main/loose_matches"=>0}],
107
+ # [[[:r, :g, :w, :c, :y], [:y, :w, :r, :w, :r]], {:"main/exact_matches"=>0, :"main/loose_matches"=>0}],
108
+ # [[[:b, :b, :r, :b, :r], [:b, :g, :c, :c, :g]], {:"main/exact_matches"=>1, :"main/loose_matches"=>0}],
109
+ # [[[:g, :r, :b, :y], [:g, :c, :r, :w]], {:"main/exact_matches"=>1, :"main/loose_matches"=>0}],
110
+
111
+ STest.check method(:score)
112
+
113
+ # [{:spec=>Speculation::FSpec(main.score), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
114
+
115
+ def self.score(secret, guess)
116
+ { ns(:exact_matches) => exact_matches(secret, guess),
117
+ ns(:loose_matches) => 0 }
118
+ end
119
+
120
+ def self.exact_matches(secret, guess)
121
+ secret.zip(guess).count { |(a, b)| a.equal?(b) }
122
+ end
123
+
124
+ S.def ns(:secret_and_guess), S.and(S.cat(:secret => ns(:code), :guess => ns(:code)),
125
+ ->(args) { args[:secret].count == args[:guess].count })
126
+
127
+ S.fdef method(:score),
128
+ :args => ns(:secret_and_guess),
129
+ :ret => S.keys(:req => [ns(:exact_matches), ns(:loose_matches)]),
130
+ :fn => ->(fn) {
131
+ sum_matches = fn[:ret].values.reduce(&:+)
132
+ sum_matches.between?(0, fn[:args][:secret].count)
133
+ }
134
+
135
+ S.fdef method(:exact_matches),
136
+ :args => ns(:secret_and_guess),
137
+ :ret => ns(S, :natural_integer),
138
+ :fn => ->(fn) { fn[:ret].between?(0, fn[:args][:secret].count) }
139
+
140
+ S.exercise_fn method(:exact_matches)
141
+
142
+ # [[[[:g, :b, :b, :b, :c], [:w, :y, :c, :y, :b]], 0],
143
+ # [[[:c, :w, :r, :c], [:y, :r, :y, :g]], 0],
144
+ # [[[:g, :c, :g, :g, :y], [:g, :g, :c, :c, :r]], 1],
145
+ # [[[:g, :r, :y, :g, :g], [:g, :y, :y, :r, :w]], 2],
146
+
147
+ STest.check method(:exact_matches)
148
+ # [{:spec=>Speculation::FSpec(main.exact_matches), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.exact_matches>}]
149
+
150
+ STest.instrument method(:exact_matches)
151
+ S.exercise_fn method(:score)
152
+
153
+ # [[[[:y, :r, :y, :r], [:g, :w, :b, :g]], {:"main/exact_matches"=>0, :"main/loose_matches"=>0}],
154
+ # [[[:c, :g, :g, :y, :b, :c], [:w, :y, :g, :b, :y, :c]], {:"main/exact_matches"=>2, :"main/loose_matches"=>0}],
155
+ # [[[:r, :b, :r, :g, :w, :r], [:c, :w, :y, :g, :g, :y]], {:"main/exact_matches"=>1, :"main/loose_matches"=>0}],
156
+ # [[[:r, :y, :c, :y, :y, :b], [:g, :r, :b, :c, :r, :y]], {:"main/exact_matches"=>0, :"main/loose_matches"=>0}],
157
+
158
+ def self.score(secret, guess)
159
+ { ns(:exact_matches) => exact_matches(secret, guess.take(3)),
160
+ ns(:loose_matches) => 0 }
161
+ end
162
+
163
+ # S.exercise_fn method(:score)
164
+
165
+ # Speculation::Error: Call to 'main.exact_matches' did not conform to spec:
166
+ # In: [1] val: [:w, :y, :c] fails spec: :"Object/code" at: [:args, :guess] predicate: [#<Method: Speculation::Utils.count_between?>, [[:w, :y, :c], 4, 6]]
167
+ # Speculation/args [[:r, :b, :c, :y, :b, :r], [:w, :y, :c]]
168
+ # Speculation/failure :instrument
169
+ # Speculation::Test/caller "(pry):69:in `score'"
170
+
171
+ def self.score(secret, guess)
172
+ { ns(:exact_matches) => exact_matches(secret, guess),
173
+ ns(:loose_matches) => 0 }
174
+ end
175
+
176
+ def match_count; end
177
+
178
+ S.fdef method(:match_count),
179
+ :args => ns(:secret_and_guess),
180
+ :ret => ns(S, :natural_integer),
181
+ :fn => ->(fn) { fn[:ret].between?(0, fn[:args][:secret].count) }
182
+
183
+ S.exercise_fn method(:exact_matches), :n => 10, :fspec => S.get_spec(method(:match_count))
184
+
185
+ # [[[[:r, :b, :g, :w, :b], [:b, :c, :c, :r, :w]], 0],
186
+ # [[[:c, :r, :c, :g, :g, :y], [:y, :c, :b, :y, :y, :r]], 0],
187
+ # [[[:c, :g, :r, :y, :y], [:w, :y, :y, :c, :w]], 0],
188
+
189
+ STest.check_method method(:exact_matches), S.get_spec(method(:match_count))
190
+
191
+ # {:spec=>Speculation::FSpec(main.match_count), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.exact_matches>}
192
+
193
+ STest.instrument method(:exact_matches), :spec => { method(:exact_matches) => S.get_spec(method(:match_count)) }
194
+
195
+ S.exercise_fn method(:score)
196
+
197
+ # [[[[:y, :r, :y, :c, :y], [:b, :w, :c, :g, :b]], {:"main/exact_matches"=>0, :"main/loose_matches"=>0}],
198
+ # [[[:r, :b, :w, :c, :w, :y], [:w, :r, :w, :g, :b, :r]], {:"main/exact_matches"=>1, :"main/loose_matches"=>0}],
199
+ # [[[:c, :w, :r, :w, :g, :c], [:g, :g, :c, :c, :c, :b]], {:"main/exact_matches"=>0, :"main/loose_matches"=>0}],
200
+
201
+ STest.check method(:score)
202
+
203
+ # [{:spec=>Speculation::FSpec(main.score), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
204
+
205
+ def self.all_matches(secret, guess)
206
+ frequencies = ->(xs) { xs.group_by(&:itself).transform_values(&:count) }
207
+ select_keys = ->(h, ks) { Hash[ks.zip(h.values_at(*ks))].compact }
208
+
209
+ select_keys.call(frequencies.call(secret), guess).
210
+ merge(select_keys.call(frequencies.call(guess), secret)) { |k, a, b| [a, b].min }.
211
+ values.
212
+ reduce(0, &:+)
213
+ end
214
+
215
+ S.exercise_fn method(:all_matches), :n => 10, :fspec => S.get_spec(method(:match_count))
216
+
217
+ # [[[[:c, :b, :w, :w], [:b, :c, :g, :y]], 2],
218
+ # [[[:y, :r, :g, :r, :b], [:r, :g, :w, :b, :r]], 4],
219
+ # [[[:r, :g, :r, :g, :g], [:r, :g, :g, :b, :c]], 3],
220
+
221
+ def self.score(secret, guess)
222
+ exact = exact_matches(secret, guess)
223
+ all = all_matches(secret, guess)
224
+
225
+ { ns(:exact_matches) => exact,
226
+ ns(:loose_matches) => all - exact }
227
+ end
228
+
229
+ STest.instrument [method(:exact_matches), method(:all_matches)],
230
+ :spec => { method(:exact_matches) => S.get_spec(method(:exact_matches)),
231
+ method(:all_matches) => S.get_spec(method(:exact_matches)) }
232
+
233
+ S.exercise_fn method(:score)
234
+ # [[[[:w, :r, :w, :c, :c, :c], [:r, :y, :r, :w, :c, :b]], {:"main/exact_matches"=>1, :"main/loose_matches"=>2}],
235
+ # [[[:r, :c, :w, :y, :c], [:g, :c, :y, :y, :y]], {:"main/exact_matches"=>2, :"main/loose_matches"=>0}],
236
+ # [[[:y, :y, :b, :g, :b], [:g, :w, :b, :c, :g]], {:"main/exact_matches"=>1, :"main/loose_matches"=>1}],
237
+ # [[[:c, :b, :r, :y, :g], [:g, :w, :r, :y, :y]], {:"main/exact_matches"=>2, :"main/loose_matches"=>1}],
238
+
239
+ STest.summarize_results STest.check method(:score)
240
+
241
+ # {:total=>1, :check_passed=>1}
242
+
243
+ result = STest.summarize_results STest.check(method(:score))
244
+ result[:total] == result[:check_passed] && !result.key?(:check_failed)
245
+
246
+ # true