zxcvbn 0.1.1 → 0.1.6

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.
@@ -6,44 +6,40 @@ module Zxcvbn
6
6
  # this calculates the average over all keys.
7
7
  def self.calc_average_degree(graph)
8
8
  average = 0
9
- graph.each do |key, neighbors|
10
- average += neighbors.count {|n| n }
9
+ graph.each do |_key, neighbors|
10
+ average += neighbors.count { |n| n }.to_f
11
11
  end
12
- average /= graph.keys.size
13
- return average
12
+ average /= graph.keys.size.to_f
13
+ average
14
14
  end
15
15
 
16
16
  BRUTEFORCE_CARDINALITY = 10
17
17
 
18
- MIN_GUESSES_BEFORE_GROWING_SEQUENCE = 10000
18
+ MIN_GUESSES_BEFORE_GROWING_SEQUENCE = 10_000
19
19
 
20
20
  MIN_SUBMATCH_GUESSES_SINGLE_CHAR = 10
21
21
 
22
22
  MIN_SUBMATCH_GUESSES_MULTI_CHAR = 50
23
23
 
24
- def self.nCk(n, k)
24
+ def self.nck(n, k) # rubocop:disable Naming/MethodParameterName
25
25
  # http://blog.plover.com/math/choose.html
26
- if k > n
27
- return 0
28
- end
29
- if k == 0
30
- return 1
31
- end
32
- r = 1
26
+ return 0.0 if k > n
27
+ return 1.0 if k == 0
28
+
29
+ r = 1.0
33
30
  (1..k).each do |d|
34
31
  r *= n
35
32
  r /= d
36
- n -= 1
33
+ n -= 1.0
37
34
  end
38
- return r
35
+ r
39
36
  end
40
37
 
41
- def self.factorial(n)
38
+ def self.factorial(n) # rubocop:disable Naming/MethodParameterName
42
39
  # unoptimized, called only on small n
43
- if n < 2
44
- return 1
45
- end
46
- return (2..10).reduce(&:*)
40
+ return 1 if n < 2
41
+
42
+ (2..n).reduce(&:*)
47
43
  end
48
44
 
49
45
  # ------------------------------------------------------------------------------
@@ -78,17 +74,17 @@ module Zxcvbn
78
74
  # D^(l-1) approximates Sum(D^i for i in [1..l-1]
79
75
 
80
76
  # ------------------------------------------------------------------------------
81
- def self.most_guessable_match_sequence(password, matches, _exclude_additive = false)
77
+ def self.most_guessable_match_sequence(password, matches, _exclude_additive: false)
82
78
  n = password.length
83
79
  # partition matches into sublists according to ending index j
84
80
  matches_by_j = (0...n).map { [] }
85
81
  matches.each do |m|
86
- matches_by_j[m[:j]] << m
82
+ matches_by_j[m["j"]] << m
87
83
  end
88
84
 
89
85
  # small detail: for deterministic output, sort each sublist by i.
90
86
  matches_by_j.each do |lst|
91
- lst.sort_by!{|m| m[:i] }
87
+ lst.sort_by! { |m| m["i"] }
92
88
  end
93
89
 
94
90
  optimal = {
@@ -96,59 +92,57 @@ module Zxcvbn
96
92
  # password prefix up to k, inclusive.
97
93
  # if there is no length-l sequence that scores better (fewer guesses) than
98
94
  # a shorter match sequence spanning the same prefix, optimal.m[k][l] is undefined.
99
- m: (0...n).map { {} },
95
+ "m" => (0...n).map { {} },
100
96
  # same structure as optimal.m -- holds the product term Prod(m.guesses for m in sequence).
101
97
  # optimal.pi allows for fast (non-looping) updates to the minimization function.
102
- pi: (0...n).map { {} },
98
+ "pi" => (0...n).map { {} },
103
99
  # same structure as optimal.m -- holds the overall metric.
104
- g: (0...n).map { {} },
100
+ "g" => (0...n).map { {} }
105
101
  }
106
102
 
107
103
  # helper: considers whether a length-l sequence ending at match m is better (fewer guesses)
108
104
  # than previously encountered sequences, updating state if so.
109
- update = -> (m, l) do
110
- k = m[:j]
105
+ update = lambda do |m, l|
106
+ k = m["j"]
111
107
  pi = estimate_guesses(m, password)
112
108
  if l > 1
113
109
  # we're considering a length-l sequence ending with match m:
114
110
  # obtain the product term in the minimization function by multiplying m's guesses
115
111
  # by the product of the length-(l-1) sequence ending just before m, at m.i - 1.
116
- pi *= optimal[:pi][m[:i] - 1][l - 1]
112
+ pi *= optimal["pi"][m["i"] - 1][l - 1]
117
113
  end
118
114
  # calculate the minimization func
119
115
  g = factorial(l) * pi
120
- if !_exclude_additive
121
- g += MIN_GUESSES_BEFORE_GROWING_SEQUENCE ** (l - 1)
122
- end
116
+ g += MIN_GUESSES_BEFORE_GROWING_SEQUENCE**(l - 1) if !_exclude_additive
123
117
  # update state if new best.
124
118
  # first see if any competing sequences covering this prefix, with l or fewer matches,
125
119
  # fare better than this sequence. if so, skip it and return.
126
- optimal[:g][k].each do |competing_l, competing_g|
127
- if competing_l > l
128
- next
129
- end
130
- if competing_g <= g
131
- return
132
- end
120
+ optimal["g"][k].find do |competing_l, competing_g|
121
+ next if competing_l > l
122
+ return nil if competing_g <= g
133
123
  end
134
124
  # this sequence might be part of the final optimal sequence.
135
- optimal[:g][k][l] = g
136
- optimal[:m][k][l] = m
137
- optimal[:pi][k][l] = pi
125
+ optimal["g"][k][l] = g
126
+ optimal["m"][k][l] = m
127
+ optimal["pi"][k][l] = pi
128
+
129
+ optimal["g"][k] = optimal["g"][k].sort.to_h
130
+ optimal["m"][k] = optimal["m"][k].sort.to_h
131
+ optimal["pi"][k] = optimal["pi"][k].sort.to_h
138
132
  end
139
133
 
140
134
  # helper: make bruteforce match objects spanning i to j, inclusive.
141
- make_bruteforce_match = -> (i, j) do
135
+ make_bruteforce_match = lambda do |i, j|
142
136
  return {
143
- pattern: 'bruteforce',
144
- token: password[i..j],
145
- i: i,
146
- j: j
137
+ "pattern" => "bruteforce",
138
+ "token" => password[i..j],
139
+ "i" => i,
140
+ "j" => j
147
141
  }
148
142
  end
149
143
 
150
144
  # helper: evaluate bruteforce matches ending at k.
151
- bruteforce_update = -> (k) do
145
+ bruteforce_update = lambda do |k|
152
146
  # see if a single bruteforce match spanning the k-prefix is optimal.
153
147
  m = make_bruteforce_match.call(0, k)
154
148
  update.call(m, 1)
@@ -156,16 +150,14 @@ module Zxcvbn
156
150
  # generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k).
157
151
  # see if adding these new matches to any of the sequences in optimal[i-1]
158
152
  # leads to new bests.
159
- m = make_bruteforce_match.call(i, k);
160
- optimal[:m][i-1].each do |l, last_m|
161
- l = l.to_i
153
+ m = make_bruteforce_match.call(i, k)
154
+ optimal["m"][i - 1].each do |l, last_m|
162
155
  # corner: an optimal sequence will never have two adjacent bruteforce matches.
163
156
  # it is strictly better to have a single bruteforce match spanning the same region:
164
157
  # same contribution to the guess product with a lower length.
165
158
  # --> safe to skip those cases.
166
- if last_m[:pattern] == 'bruteforce'
167
- next
168
- end
159
+ next if last_m["pattern"] == "bruteforce"
160
+
169
161
  # try adding m to this length-l sequence.
170
162
  update.call(m, l + 1)
171
163
  end
@@ -174,15 +166,15 @@ module Zxcvbn
174
166
 
175
167
  # helper: step backwards through optimal.m starting at the end,
176
168
  # constructing the final optimal match sequence.
177
- unwind = -> (n) do
169
+ unwind = lambda do |n2|
178
170
  optimal_match_sequence = []
179
- k = n - 1
171
+ k = n2 - 1
180
172
  # find the final best sequence length and score
181
- l, g = (optimal[:g][k] || []).min_by{|candidate_l, candidate_g| candidate_g || 0 }
173
+ l, _g = (optimal["g"][k] || []).min_by { |_candidate_l, candidate_g| candidate_g || 0 }
182
174
  while k >= 0
183
- m = optimal[:m][k][l]
175
+ m = optimal["m"][k][l]
184
176
  optimal_match_sequence.unshift(m)
185
- k = m[:i] - 1
177
+ k = m["i"] - 1
186
178
  l -= 1
187
179
  end
188
180
  return optimal_match_sequence
@@ -190,8 +182,8 @@ module Zxcvbn
190
182
 
191
183
  (0...n).each do |k|
192
184
  matches_by_j[k].each do |m|
193
- if m[:i] > 0
194
- optimal[:m][m[:i] - 1].keys.each do |l|
185
+ if m["i"] > 0
186
+ optimal["m"][m["i"] - 1].each_key do |l|
195
187
  update.call(m, l + 1)
196
188
  end
197
189
  else
@@ -205,18 +197,18 @@ module Zxcvbn
205
197
  optimal_l = optimal_match_sequence.length
206
198
 
207
199
  # corner: empty password
208
- if password.length == 0
209
- guesses = 1
200
+ guesses = if password.empty?
201
+ 1
210
202
  else
211
- guesses = optimal[:g][n - 1][optimal_l]
203
+ optimal["g"][n - 1][optimal_l]
212
204
  end
213
205
 
214
206
  # final result object
215
- return {
216
- password: password,
217
- guesses: guesses,
218
- guesses_log10: Math.log10(guesses),
219
- sequence: optimal_match_sequence
207
+ {
208
+ "password" => password,
209
+ "guesses" => guesses,
210
+ "guesses_log10" => Math.log10(guesses),
211
+ "sequence" => optimal_match_sequence
220
212
  }
221
213
  end
222
214
 
@@ -224,76 +216,73 @@ module Zxcvbn
224
216
  # guess estimation -- one function per match pattern ---------------------------
225
217
  # ------------------------------------------------------------------------------
226
218
  def self.estimate_guesses(match, password)
227
- if match[:guesses]
228
- return match[:guesses] # a match's guess estimate doesn't change. cache it.
219
+ if match["guesses"]
220
+ return match["guesses"] # a match's guess estimate doesn't change. cache it.
229
221
  end
222
+
230
223
  min_guesses = 1
231
- if match[:token].length < password.length
232
- min_guesses = if match[:token].length == 1
224
+ if match["token"].length < password.length
225
+ min_guesses = if match["token"].length == 1
233
226
  MIN_SUBMATCH_GUESSES_SINGLE_CHAR
234
227
  else
235
228
  MIN_SUBMATCH_GUESSES_MULTI_CHAR
236
229
  end
237
230
  end
238
231
  estimation_functions = {
239
- bruteforce: method(:bruteforce_guesses),
240
- dictionary: method(:dictionary_guesses),
241
- spatial: method(:spatial_guesses),
242
- repeat: method(:repeat_guesses),
243
- sequence: method(:sequence_guesses),
244
- regex: method(:regex_guesses),
245
- date: method(:date_guesses),
232
+ "bruteforce" => method(:bruteforce_guesses),
233
+ "dictionary" => method(:dictionary_guesses),
234
+ "spatial" => method(:spatial_guesses),
235
+ "repeat" => method(:repeat_guesses),
236
+ "sequence" => method(:sequence_guesses),
237
+ "regex" => method(:regex_guesses),
238
+ "date" => method(:date_guesses)
246
239
  }
247
- guesses = estimation_functions[match[:pattern].to_sym].call(match)
248
- match[:guesses] = [guesses, min_guesses].max
249
- match[:guesses_log10] = Math.log10(match[:guesses])
250
- return match[:guesses]
240
+ guesses = estimation_functions[match["pattern"]].call(match)
241
+ match["guesses"] = [guesses, min_guesses].max
242
+ match["guesses_log10"] = Math.log10(match["guesses"])
243
+ match["guesses"]
251
244
  end
252
245
 
253
- MAX_VALUE = 2 ** 1024
246
+ MAX_VALUE = 2**1024
254
247
 
255
248
  def self.bruteforce_guesses(match)
256
- guesses = BRUTEFORCE_CARDINALITY ** match[:token].length
249
+ guesses = BRUTEFORCE_CARDINALITY**match["token"].length
257
250
  # trying to match JS behaviour here setting a MAX_VALUE to try to acheieve same values as JS library.
258
- if guesses > MAX_VALUE
259
- guesses = MAX_VALUE
260
- end
251
+ guesses = MAX_VALUE if guesses > MAX_VALUE
261
252
 
262
253
  # small detail: make bruteforce matches at minimum one guess bigger than smallest allowed
263
254
  # submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence.
264
- min_guesses = if match[:token].length == 1
255
+ min_guesses = if match["token"].length == 1
265
256
  MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1
266
257
  else
267
258
  MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1
268
259
  end
269
260
 
270
- [guesses, min_guesses].max
261
+ [guesses, min_guesses].max.to_f
271
262
  end
272
263
 
273
264
  def self.repeat_guesses(match)
274
- return match[:base_guesses] * match[:repeat_count]
265
+ match["base_guesses"] * match["repeat_count"]
275
266
  end
276
267
 
277
268
  def self.sequence_guesses(match)
278
- first_chr = match[:token][0]
269
+ first_chr = match["token"][0]
279
270
  # lower guesses for obvious starting points
280
- if ['a', 'A', 'z', 'Z', '0', '1', '9'].include?(first_chr)
281
- base_guesses = 4
271
+ base_guesses = if ["a", "A", "z", "Z", "0", "1", "9"].include?(first_chr)
272
+ 4
273
+ elsif first_chr.match?(/\d/)
274
+ 10
282
275
  else
283
- if first_chr.match?(/\d/)
284
- base_guesses = 10 # digits
285
- else
286
- # could give a higher base for uppercase,
287
- # assigning 26 to both upper and lower sequences is more conservative.
288
- base_guesses = 26
289
- end
276
+ # could give a higher base for uppercase,
277
+ # assigning 26 to both upper and lower sequences is more conservative.
278
+ 26
290
279
  end
291
- if !match[:ascending]
280
+ if !match["ascending"]
292
281
  # need to try a descending sequence in addition to every ascending sequence ->
293
282
  # 2x guesses
294
283
  base_guesses *= 2
295
284
  end
296
- return base_guesses * match[:token].length
285
+ base_guesses * match["token"].length
297
286
  end
298
287
 
299
288
  MIN_YEAR_SPACE = 20
@@ -301,126 +290,123 @@ module Zxcvbn
301
290
 
302
291
  def self.regex_guesses(match)
303
292
  char_class_bases = {
304
- alpha_lower: 26,
305
- alpha_upper: 26,
306
- alpha: 52,
307
- alphanumeric: 62,
308
- digits: 10,
309
- symbols: 33
293
+ "alpha_lower" => 26,
294
+ "alpha_upper" => 26,
295
+ "alpha" => 52,
296
+ "alphanumeric" => 62,
297
+ "digits" => 10,
298
+ "symbols" => 33
310
299
  }
311
- if char_class_bases.has_key? match[:regex_name].to_sym
312
- return char_class_bases[match[:regex_name].to_sym] ** match[:token].length
313
- elsif match[:regex_name] == 'recent_year'
300
+ if char_class_bases.key? match["regex_name"]
301
+ char_class_bases[match["regex_name"]]**match["token"].length
302
+ elsif match["regex_name"] == "recent_year"
314
303
  # conservative estimate of year space: num years from REFERENCE_YEAR.
315
304
  # if year is close to REFERENCE_YEAR, estimate a year space of MIN_YEAR_SPACE.
316
- year_space = (match[:regex_match[0]].to_i - REFERENCE_YEAR).abs
317
- year_space = [year_space, MIN_YEAR_SPACE].max
318
- return year_space
305
+ year_space = (match["regex_match"][0].to_i - REFERENCE_YEAR).abs
306
+ [year_space, MIN_YEAR_SPACE].max
307
+
319
308
  end
320
309
  end
321
310
 
322
311
  def self.date_guesses(match)
323
312
  # base guesses: (year distance from REFERENCE_YEAR) * num_days * num_years
324
- year_space = [(match[:year] - REFERENCE_YEAR).abs, MIN_YEAR_SPACE].max
313
+ year_space = [(match["year"] - REFERENCE_YEAR).abs, MIN_YEAR_SPACE].max
325
314
  guesses = year_space * 365
326
- if match[:separator]
315
+ separator = match["separator"]
316
+ if !["", nil].include?(separator)
327
317
  # add factor of 4 for separator selection (one of ~4 choices)
328
318
  guesses *= 4
329
319
  end
330
- return guesses
320
+ guesses
331
321
  end
332
322
 
333
- KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS[:qwerty])
323
+ KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["qwerty"]).freeze
334
324
  # slightly different for keypad/mac keypad, but close enough
335
- KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS[:keypad])
325
+ KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["keypad"]).freeze
336
326
 
337
- KEYBOARD_STARTING_POSITIONS = ADJACENCY_GRAPHS[:qwerty].keys.size
338
- KEYPAD_STARTING_POSITIONS = ADJACENCY_GRAPHS[:keypad].keys.size
327
+ KEYBOARD_STARTING_POSITIONS = ADJACENCY_GRAPHS["qwerty"].keys.size
328
+ KEYPAD_STARTING_POSITIONS = ADJACENCY_GRAPHS["keypad"].keys.size
339
329
 
340
330
  def self.spatial_guesses(match)
341
- if ['qwerty', 'dvorak'].include?(match[:graph])
342
- s = KEYBOARD_STARTING_POSITIONS;
343
- d = KEYBOARD_AVERAGE_DEGREE;
331
+ if ["qwerty", "dvorak"].include?(match["graph"])
332
+ s = KEYBOARD_STARTING_POSITIONS
333
+ d = KEYBOARD_AVERAGE_DEGREE
344
334
  else
345
- s = KEYPAD_STARTING_POSITIONS;
346
- d = KEYPAD_AVERAGE_DEGREE;
335
+ s = KEYPAD_STARTING_POSITIONS
336
+ d = KEYPAD_AVERAGE_DEGREE
347
337
  end
348
- guesses = 0
349
- ll = match[:token].length
350
- t = match[:turns]
338
+ guesses = 0.0
339
+ ll = match["token"].length
340
+ t = match["turns"]
351
341
  # estimate the number of possible patterns w/ length ll or less with t turns or less.
352
342
  (2..ll).each do |i|
353
343
  possible_turns = [t, i - 1].min
354
344
  (1..possible_turns).each do |j|
355
- guesses += nCk(i - 1, j - 1) * s * (d ** j)
345
+ guesses += nck((i - 1).to_f, (j - 1).to_f) * s.to_f * (d.to_f**j.to_f)
356
346
  end
357
347
  end
358
348
  # add extra guesses for shifted keys. (% instead of 5, A instead of a.)
359
349
  # math is similar to extra guesses of l33t substitutions in dictionary matches.
360
- if match[:shifted_count]
361
- ss = match[:shifted_count]
362
- uu = match[:token].length - match[:shifted_count] # unshifted count
350
+ if match["shifted_count"] && match["shifted_count"] != 0
351
+ ss = match["shifted_count"]
352
+ uu = match["token"].length - match["shifted_count"] # unshifted count
363
353
  if ss == 0 || uu == 0
364
354
  guesses *= 2
365
355
  else
366
356
  shifted_variations = 0
367
357
  (1..[ss, uu].min).each do |i|
368
- shifted_variations += nCk(ss + uu, i)
358
+ shifted_variations += nck((ss + uu).to_f, i.to_f)
369
359
  end
370
360
  guesses *= shifted_variations
371
361
  end
372
362
  end
373
- return guesses
363
+ guesses
374
364
  end
375
365
 
376
366
  def self.dictionary_guesses(match)
377
- match[:base_guesses] = match[:rank] # keep these as properties for display purposes
378
- match[:uppercase_variations] = uppercase_variations(match)
379
- match[:l33t_variations] = l33t_variations(match)
380
- reversed_variations = match[:reversed] && 2 || 1
381
- return match[:base_guesses] * match[:uppercase_variations] * match[:l33t_variations] * reversed_variations
367
+ match["base_guesses"] = match["rank"] # keep these as properties for display purposes
368
+ match["uppercase_variations"] = uppercase_variations(match)
369
+ match["l33t_variations"] = l33t_variations(match)
370
+ reversed_variations = match["reversed"] && 2 || 1
371
+ match["base_guesses"] * match["uppercase_variations"] * match["l33t_variations"] * reversed_variations
382
372
  end
383
373
 
384
- START_UPPER = /^[A-Z][^A-Z]+$/
385
- END_UPPER = /^[^A-Z]+[A-Z]$/
386
- ALL_UPPER = /^[^a-z]+$/
387
- ALL_LOWER = /^[^A-Z]+$/
374
+ START_UPPER = /^[A-Z][^A-Z]+$/.freeze
375
+ END_UPPER = /^[^A-Z]+[A-Z]$/.freeze
376
+ ALL_UPPER = /^[^a-z]+$/.freeze
377
+ ALL_LOWER = /^[^A-Z]+$/.freeze
388
378
 
389
379
  def self.uppercase_variations(match)
390
- word = match[:token]
391
- if word.match?(ALL_LOWER) || word.downcase === word
392
- return 1
393
- end
380
+ word = match["token"]
381
+ return 1 if word.match?(ALL_LOWER) || word.downcase == word
382
+
394
383
  # a capitalized word is the most common capitalization scheme,
395
384
  # so it only doubles the search space (uncapitalized + capitalized).
396
385
  # allcaps and end-capitalized are common enough too, underestimate as 2x factor to be safe.
397
386
  [START_UPPER, END_UPPER, ALL_UPPER].each do |regex|
398
- if word.match?(regex)
399
- return 2
400
- end
387
+ return 2 if word.match?(regex)
401
388
  end
402
389
  # otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters
403
390
  # with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD),
404
391
  # the number of ways to lowercase U+L letters with L lowercase letters or less.
405
- uu = word.split("").count{|chr| chr.match?(/[A-Z]/)}
406
- ll = word.split("").count{|chr| chr.match?(/[a-z]/)}
407
- variations = 0;
392
+ uu = word.chars.count { |chr| chr.match?(/[A-Z]/) }
393
+ ll = word.chars.count { |chr| chr.match?(/[a-z]/) }
394
+ variations = 0
408
395
  (1..[uu, ll].min).each do |i|
409
- variations += nCk(uu + ll, i)
396
+ variations += nck(uu + ll, i)
410
397
  end
411
- return variations
398
+ variations
412
399
  end
413
400
 
414
401
  def self.l33t_variations(match)
415
- if !match[:l33t]
416
- return 1
417
- end
402
+ return 1 if !match["l33t"]
403
+
418
404
  variations = 1
419
- match[:sub].each do |subbed, unsubbed|
405
+ match["sub"].each do |subbed, unsubbed|
420
406
  # lower-case match.token before calculating: capitalization shouldn't affect l33t calc.
421
- chrs = match[:token].downcase.split('')
422
- ss = chrs.count{|chr| chr == subbed }
423
- uu = chrs.count{|chr| chr == unsubbed }
407
+ chrs = match["token"].downcase.chars
408
+ ss = chrs.count { |chr| chr == subbed }
409
+ uu = chrs.count { |chr| chr == unsubbed }
424
410
  if ss == 0 || uu == 0
425
411
  # for this sub, password is either fully subbed (444) or fully unsubbed (aaa)
426
412
  # treat that as doubling the space (attacker needs to try fully subbed chars in addition to
@@ -432,12 +418,12 @@ module Zxcvbn
432
418
  p = [uu, ss].min
433
419
  possibilities = 0
434
420
  (1..p).each do |i|
435
- possibilities += nCk(uu + ss, i)
421
+ possibilities += nck(uu + ss, i)
436
422
  end
437
423
  variations *= possibilities
438
424
  end
439
425
  end
440
- return variations
426
+ variations
441
427
  end
442
428
  end
443
429
  end