Strange Ruby 2+ Behavior with "select!" - ruby

I'm having an issue that I can't seem to find documented or explained anywhere so I'm hoping someone here can help me out. I've verified the unexpected behavior on three versions of Ruby, all 2.1+, and verified that it doesn't happen on an earlier version (though it's through tryruby.org and I don't know which version they're using). Anyway, for the question I'll just post some code with results and hopefully someone can help me debug it.
arr = %w( r a c e c a r ) #=> ["r","a","c","e","c","a","r"]
arr.select { |c| arr.count(c).odd? } #=> ["e"]
arr.select! { |c| arr.count(c).odd? } #=> ["e","r"] <<<<<<<<<<<<<<< ??????
I think the confusing part for me is clearly marked and if anyone can explain if this is a bug or if there's some logic to it, I'd greatly appreciate it. Thanks!

You're modifying the array while you're read from it while you iterate over it. I'm not sure the result is defined behavior. The algorithm isn't required to keep the object in any kind of sane state while it's running.
Some debug printing during the iteration shows why your particular result happens:
irb(main):005:0> x
=> ["r", "a", "c", "e", "c", "a", "r"]
irb(main):006:0> x.select! { |c| p x; x.count(c).odd? }
["r", "a", "c", "e", "c", "a", "r"]
["r", "a", "c", "e", "c", "a", "r"]
["r", "a", "c", "e", "c", "a", "r"]
["r", "a", "c", "e", "c", "a", "r"] # "e" is kept...
["e", "a", "c", "e", "c", "a", "r"] # ... and moved to the start of the array
["e", "a", "c", "e", "c", "a", "r"]
["e", "a", "c", "e", "c", "a", "r"] # now "r" is kept
=> ["e", "r"]
You can see by the final iteration, there is only one r, and that the e has been moved to the front of the array. Presumably the algorithm modifies the array in-place, moving matched elements to the front, overwriting elements that have already failed your test. It keeps track of how many elements are matched and moved, and then truncates the array down to that many elements.
So, instead, use select.
A longer example that matches more elements makes the problem a little clearer:
irb(main):001:0> nums = (1..10).to_a
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
irb(main):002:0> nums.select! { |i| p nums; i.even? }
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[2, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[2, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[2, 4, 3, 4, 5, 6, 7, 8, 9, 10]
[2, 4, 3, 4, 5, 6, 7, 8, 9, 10]
[2, 4, 6, 4, 5, 6, 7, 8, 9, 10]
[2, 4, 6, 4, 5, 6, 7, 8, 9, 10]
[2, 4, 6, 8, 5, 6, 7, 8, 9, 10]
[2, 4, 6, 8, 5, 6, 7, 8, 9, 10]
=> [2, 4, 6, 8, 10]
You can see that it does indeed move matched elements to the front of the array, overwriting non-matched elements, and then truncate the array.

Just to give you some other ways of accomplishing what you're doing:
arr = %w( r a c e c a r )
arr.group_by{ |c| arr.count(c).odd? }
# => {false=>["r", "a", "c", "c", "a", "r"], true=>["e"]}
arr.group_by{ |c| arr.count(c).odd? }.values
# => [["r", "a", "c", "c", "a", "r"], ["e"]]
arr.partition{ |c| arr.count(c).odd? }
# => [["e"], ["r", "a", "c", "c", "a", "r"]]
And if you want more readable keys:
arr.group_by{ |c| arr.count(c).odd? ? :odd : :even }
# => {:even=>["r", "a", "c", "c", "a", "r"], :odd=>["e"]}
partition and group_by are basic building blocks for separating elements in an array into some sort of grouping, so it is good to be familiar with them.

Related

How to group array elements by index?

I have the array arr which I want to group by indexes given in array idx. I mean,
sub array 1 will end at index 1
sub array 2 will end at index 5
sub array 3 will end at index 7
sub array N will be formed from element at index 8 to last element
of arr
With my current code I can group the first sub array with first index of idx idx[0] = 1.
Then, How to replicate for all indexes within array idx? Thanks in advance.
My current code and output is this:
idx = [1,5,7]
arr = ['a','b','c','d','e','f','g','h','i','j','k']
arr.group_by.with_index { |z, i| i <= idx[0] }.values
=> [["a", "b"], ["c", "d", "e", "f", "g", "h", "i", "j", "k"]]
and my desired output is like this:
output --> [["a", "b"], ["c", "d", "e", "f"], ["g", "h"], ["i", "j", "k"]]
#Indexes --> 0 1 2 3 4 5 6 7 8 9 10
You can use slice_after to slice the array after each item whose index is in idx:
idx = [1, 5, 7]
arr = %w[a b c d e f g h i j k]
arr.enum_for(:slice_after).with_index { |_, i| idx.include?(i) }.to_a
#=> [["a", "b"], ["c", "d", "e", "f"], ["g", "h"], ["i", "j", "k"]]
That enum_for is (unfortunately) needed to chain slice_after and with_index.
Another solution
idx = [1, 5, 7]
arr = ['a','b','c','d','e','f','g','h','i','j','k']
from = 0
arr.map.with_index { |a, i|
if idx.include?(i)
result = arr[from..i]
from = i + 1
end
result
}.compact
=> [["a", "b"], ["c", "d", "e", "f"], ["g", "h"]]
arr = [9, 3, 1, 6, 2, 4, 0, 1, 5, 8]
end_idx = [1, 5, 7]
[-1, *end_idx, arr.size-1].uniq.each_cons(2).
map { |s,e| arr.values_at(s+1..e) }
#=> [[9, 3], [1, 6, 2, 4], [0, 1], [5, 8]]
The steps are as follow:
a = [-1, *end_idx, arr.size-1]
#=> [-1, 1, 5, 7, 9]
b = a.uniq
#=> [-1, 1, 5, 9]
c = b.each_cons(2)
#=> #<Enumerator: [-1, 1, 5, 7, 9]:each_cons(2)>
c.map { |s,e| arr.values_at(s+1..e) }
#=> [[9, 3], [1, 6, 2, 4], [0, 1], [5, 8]]
One can see the elements generated and passed to map by the enumerator c by converting c to an array.
c.to_a
#=> [[-1, 1], [1, 5], [5, 7], [7, 9]]
See Array#values_at.

How does one create a loop with indefinite nested loops?

say you have a list [ 1 , 2 ,3 ...... n]
if you needed to compare two elements so you would write something like
list = (0..9999).to_a
idx = 0
while idx < list.length
idx2 = idx
while idx2 < list.length
puts list[idx] + list[idx2] if (list[idx] + list[idx2]).odd?
idx2 += 1
end
idx += 1
end
But what if the number of comparisons is not constant and increases?
This code hard codes the comparison by having one loop inside another, but if you needed to compare 4 or more elements how does one write a loop or something that achieves this if you don't know the maximum number of comparisons?
We have a helpful method in ruby to do this, and that is Array#combination:
def find_odd_sums(list, num_per_group)
list.combination(num_per_group).to_a.map(&:sum).select(&:odd?)
end
You can re-implement combination, if you choose to. There are many versions of this function available at Algorithm to return all combinations of k elements from n
This question is not clear. Firstly, the title, which is vague, asks how a particular approach to an unstated problem can be implemented. What you need, at the beginning, is a statement in words of the problem.
I will make a guess as to what that statement might be and then propose a solution.
Given
an array arr;
a positive integer n, 1 <= n <= arr.size; and
a method m having n arguments that are distinct elements of arr that returns true or false,
what combinations of n elements of arr cause m to return true?
We can use the following method combined with a definition of the method m.
def combos(arr, n, m)
arr.combination(n).select { |x| public_send(m, *x) }
end
The key, of course, is the method Array#combination. See also the docs for the methods Enumerable#select and Object#public_send.
Here is its use with the example given in the question.
def m(*x)
x.sum.odd?
end
arr = [1,2,3,4,5,6]
combos(arr, 2, :m)
#=> [[1, 2], [1, 4], [1, 6], [2, 3], [2, 5], [3, 4], [3, 6], [4, 5], [5, 6]]
combos(arr, 3, :m)
#=> [[1, 2, 4], [1, 2, 6], [1, 3, 5], [1, 4, 6], [2, 3, 4], [2, 3, 6],
# [2, 4, 5], [2, 5, 6], [3, 4, 6], [4, 5, 6]]
combos(arr, 4, :m)
#=> [[1, 2, 3, 5], [1, 2, 4, 6], [1, 3, 4, 5], [1, 3, 5, 6], [2, 3, 4, 6], [2, 4, 5, 6]]
See the doc for Array#sum (which made it's debut in Ruby v2.4.
Here's a second example: given an array of letters, which combinations of five letters have two vowels?
VOWEL_COUNTER = %w| a e i o u |.product([1]).to_h.tap { |h| h.default=0 }
#=> {"a"=>1, "e"=>1, "i"=>1, "o"=>1, "u"=>1}
VOWEL_COUNTER['a']
#=> 1
By setting the hash's default value to zero, VOWEL_COUNTER[k] will return zero if it does not have a key k. For example,
VOWEL_COUNTER['r']
#=> 0
def m(*x)
x.sum { |c| VOWEL_COUNTER[c] } == 2
end
arr = %w| a r t u e v s |
combos(arr, 5, :m)
#=> [["a", "r", "t", "u", "v"], ["a", "r", "t", "u", "s"],
# ["a", "r", "t", "e", "v"], ["a", "r", "t", "e", "s"],
# ["a", "r", "u", "v", "s"], ["a", "r", "e", "v", "s"],
# ["a", "t", "u", "v", "s"], ["a", "t", "e", "v", "s"],
# ["r", "t", "u", "e", "v"], ["r", "t", "u", "e", "s"],
# ["r", "u", "e", "v", "s"], ["t", "u", "e", "v", "s"]]
Note that VOWEL_COUNTER is constructed as follows.
a = %w| a e i o u |
#=> ["a", "e", "i", "o", "u"]
b = a.product([1])
#=> [["a", 1], ["e", 1], ["i", 1], ["o", 1], ["u", 1]]
c = b.to_h
#=> {"a"=>1, "e"=>1, "i"=>1, "o"=>1, "u"=>1}
With this hash,
c['r']
#=> nil
so we need to set the default value to zero.
VOWEL_COUNTER = c.tap { |h| h.default=0 }
#=> {"a"=>1, "e"=>1, "i"=>1, "o"=>1, "u"=>1}
c['r']
#=> 0
Alternatively, we could have omitted the last step (setting the hash's default to zero), and written
x.sum { |c| VOWEL_COUNTER[c].to_i } == 2
because NilClass#to_i converts nil to zero.
See also the docs for the methods #select, #public_send
I feel like everyone is making this more complicated than it is. You sure got pointed to the right direction (Array#combination, Array#repeated_combination, Array#permutation, Array#repeated_permutation). To accomplish the exact thing you are doing, you can simply do:
list.repeated_combination(2) { |c| puts c.sum if c.sum.odd? }
Check the links above to see the difference between them.
If you want to create a helper method you can, but in my opinion it's not really needed in this case. Replace 2 with the number you are looking for and you got your answer.

Find all repeating non-overlapping substrings and cycles

I have a complex problem of string manipulation at hand.
I have a string in which I will have cycles, as well as recurrences which I need to identify and list down.
'abcabcabcabcabcdkkabclilabcoabcdieabcdowabcdppabzabx'
Following are the possible patterns ->
Actual indexes not used
abc -> 0,3,6,9,12,15,17, ..... (occurence index for recurring string),
0,3,6,9 (unique_occurence index for recurring string, 12, 15, 17
disqualified as there abc was a part of longer repeating substring)
abcd -> 12, 15, 17 (occurence index for recurring string), 12, 15, 17
(unique occurence index for recurring string)
bcda -> 13, 16, 18.. (occurence index for recurring string), (unique occurence index for recurring string) as it is an overlap for
the string abcd Hence it is something not required ab ->
0,3,6,9,12,15,17, 25, 27 ...(occurence index for recurring string),
25, 27(unique occurence index for recurring string). .....
I want to find all unique recurring occurences/recurrences, i.e. All Unique, Non-Overlapping values of recurring string. As mentioned above. And the input string may contain,
ALL cyclic patterns(abcabcabcdefdefdeflkjlkjlkj => abc, def, lkj are recurrences in cycle, but bc, ab, bcab are not expected as they are outcomes of false positives)
OR
Separately recurring patterns(abcxabcdabcm => abc is recurrence but not cycle, i.e. they are not adjecent)
Or
A mix of both(abcabcabcabcabclkabcdokabcdhuabcd => abc is a cyclic recurrence, and abcd is a non cyclic recurrence and we need to find both -> only abcd, abc are recurring, not bc, ab, bcda, etc)
Can someone propose a solution algo for this problem statement. I am trying using suffix_arrays which is not finding overlapping results as well.
A hash is constructed whose keys consist of all unique substrings of a given string that appear at least twice in the string (not overlapping) and, for each key, the value is an array of all offsets into the string where the value of the key (a substring) begins.
Code
def recurring_substrings(str)
arr = str.chars
(1..str.size/2).each_with_object({}) do |n,h|
arr.each_cons(n).map { |b| b.join }.uniq.each do |s|
str.scan(Regexp.new(s)) { (h[s] ||= []) << Regexp.last_match.begin(0) }
end
end.reject { |_,v| v.size == 1 }
end
Examples
recurring_substrings 'abjkabrjkab'
#=> {"a"=>[0, 4, 9], "b"=>[1, 5, 10], "j"=>[2, 7], "k"=>[3, 8], "ab"=>[0, 4, 9],
# "jk"=>[2, 7], "ka"=>[3, 8], "jka"=>[2, 7], "kab"=>[3, 8], "jkab"=>[2, 7]}
recurring_substrings "abcabcabcabcabcdkkabclilabcoabcdieabcdowabcdppabzabx"
#=> {"a"=>[0, 3, 6, 9, 12, 18, 24, 28, 34, 40, 46, 49],
# "b"=>[1, 4, 7, 10, 13, 19, 25, 29, 35, 41, 47, 50],
# "c"=>[2, 5, 8, 11, 14, 20, 26, 30, 36, 42], "d"=>[15, 31, 37, 43],
# "k"=>[16, 17], "l"=>[21, 23], "i"=>[22, 32], "o"=>[27, 38], "p"=>[44, 45],
# "ab"=>[0, 3, 6, 9, 12, 18, 24, 28, 34, 40, 46, 49],
# "bc"=>[1, 4, 7, 10, 13, 19, 25, 29, 35, 41], "ca"=>[2, 5, 8, 11],
# "cd"=>[14, 30, 36, 42],
# "abc"=>[0, 3, 6, 9, 12, 18, 24, 28, 34, 40], "bca"=>[1, 4, 7, 10],
# "cab"=>[2, 5, 8, 11], "bcd"=>[13, 29, 35, 41],
# "abca"=>[0, 6], "bcab"=>[1, 7], "cabc"=>[2, 8], "abcd"=>[12, 28, 34, 40],
# "abcab"=>[0, 6], "bcabc"=>[1, 7], "cabca"=>[2, 8],
# "abcabc"=>[0, 6], "bcabca"=>[1, 7], "cabcab"=>[2, 8]}
Explanation
For the first example above, the steps are as follows.
str = 'abjkabrjkab'
arr = str.chars
#=> ["a", "b", "j", "k", "a", "b", "r", "j", "k", "a", "b"]
q = str.size/2 # max size for string to repeat at least once
#=> 5
b = (1..q).each_with_object({})
#=> #<Enumerator: 1..5:each_with_object({})>
We can see which elements will be generated by this enumerator by converting it to an array. (I will do this a few more times below.)
b.to_a
#=> [[1, {}], [2, {}], [3, {}], [4, {}], [5, {}]]
The empty hashes will be built up as calculations progress.
Next pass the first element to the block and set the block variables to it using parallel assignment (sometimes called multiple assignment).
n,h = b.next
#=> [1, {}]
n #=> 1
h #=> {}
c = arr.each_cons(n)
#=> #<Enumerator: ["a", "b", "j", "k", "a", "b", "r", "j", "k", "a", "b"]:each_cons(1)>
c is an array of all substrings of length 1. At the next iteration it will be an array of all substrings of length 2 and so on. See Emumerable#each_cons.
c.to_a # Let's see which elements will be generated.
#=> [["a"], ["b"], ["j"], ["k"], ["a"], ["b"], ["r"], ["j"], ["k"], ["a"], ["b"]]
d = c.map { |b| b.join }
#=> ["a", "b", "j", "k", "a", "b", "r", "j", "k", "a", "b"]
e = d.uniq
#=> ["a", "b", "j", "k", "r"]
At the next iteration this will be
r = arr.each_cons(2)
#=> #<Enumerator: ["a", "b", "j", "k", "a", "b", "r", "j", "k", "a", "b"]:
# each_cons(2)>
r.to_a
#=> [["a", "b"], ["b", "j"], ["j", "k"], ["k", "a"], ["a", "b"],
# ["b", "r"], ["r", "j"], ["j", "k"], ["k", "a"], ["a", "b"]]
s = r.map { |b| b.join }
#=> ["ab", "bj", "jk", "ka", "ab", "br", "rj", "jk", "ka", "ab"]
s.uniq
#=> ["ab", "bj", "jk", "ka", "br", "rj"]
Continuing,
f = e.each
#=> #<Enumerator: ["a", "b", "j", "k", "r"]:each>
f.to_a # Let's see which elements will be generated.
#=> ["a", "b", "j", "k", "r"]
s = f.next
#=> "a"
r = (Regexp.new(s))
#=> /a/
str.scan(r) { (h[s] ||= []) << Regexp.last_match.begin(0) }
If h does not yet have a key s, h[s] #=> nil. h[s] ||= [], which expands to h[s] = h[s] || [], converts h[s] to an empty array before executing h[s] << Regexp.last_match.begin(0). That is, h[s] = h[s] || [] #=> nil || [] #=> [].
Within the block the MatchData object is retrieved with the class method Regexp::last_match. (Alternatively, one could substitute the global variable $~ for Regexp.last_match. For details, search for "special global variables" at Regexp.) MatchData#begin returns the index of str at which the current match begins.
Now
h #=> {"a"=>[0, 4, 9]}
The remaining calculations are similar, adding key-value pairs to h until the has given in the example has been constructed.
For further processing after #CarySwoveland's excellent answer :
def ignore_smaller_substrings(hash)
found_indices = []
new_hash = {}
hash.sort_by{|s,_| [-s.size,s]}.each{|s,indices|
indices -= found_indices
found_indices |= indices
new_hash[s]=indices unless indices.empty?
}
new_hash
end
pp ignore_smaller_substrings(recurring_substrings('abcabcabcabcabcdkkabclilabcoabcdieabcdowabcdppabzabx'))
Hash is sorted by decreasing string length (and then alphabetically), and indices are only allowed to appear once.
It outputs
{"abcabc"=>[0, 6],
"bcabca"=>[1, 7],
"cabcab"=>[2, 8],
"abcd"=>[12, 28, 34, 40],
"abc"=>[3, 9, 18, 24],
"bca"=>[4, 10],
"bcd"=>[13, 29, 35, 41],
"cab"=>[5, 11],
"ab"=>[46, 49],
"bc"=>[19, 25],
"cd"=>[14, 30, 36, 42],
"b"=>[47, 50],
"c"=>[20, 26],
"d"=>[15, 31, 37, 43],
"i"=>[22, 32],
"k"=>[16, 17],
"l"=>[21, 23],
"o"=>[27, 38],
"p"=>[44, 45]}
It doesn't answer the question exactly, but it comes a bit closer.

How do you check an array for a range in Ruby?

I'm writing a poker program, and I can't figure out how to handle straights.
Straight: All cards in a hand of 5 cards are consecutive values.
ex. 2..6, 3..7, 4..8, 5..9, 6..T, 7..J, 8..Q, 9..K, T..A
cards = [2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A"]
How can I check a hand, which is an array, for these combinations? Preferably I can check it to see if it's 5 in a row in the cards array.
Edit 2: This is my absolutely final solution:
require 'set'
STRAIGHTS = ['A',*2..9,'T','J','Q','K','A'].each_cons(5).map(&:to_set)
#=> [#<Set: {"A", 2, 3, 4, 5}>, #<Set: {2, 3, 4, 5, 6}>,
# ...#<Set: {9, "T", "J", "Q", "K"}>, #<Set: {"T", "J", "Q", "K", "A"}>]
def straight?(hand)
STRAIGHTS.include?(hand.to_set)
end
STRAIGHTS.include?([6,3,4,5,2].to_set)
# STRAIGHTS.include?(#<Set: {6, 3, 4, 5, 2}>)
#=> true
straight?([6,5,4,3,2]) #=> true
straight?(["T","J","Q","K","A"]) #=> true
straight?(["A","K","Q","J","T"]) #=> true
straight?([2,3,4,5,"A"]) #=> true
straight?([6,7,8,9,"J"]) #=> false
straight?(["J",7,8,9,"T"]) #=> false
Edit 1: #mudasobwa upset the apple cart by pointing out that 'A',2,3,4,5 is a valid straight. I believe I've fixed my answer. (I trust he's not going to tell me that 'K','A',2,3,4 is also valid.)
I would suggest the following:
CARDS = [2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A"]
STRAIGHTS = CARDS.each_cons(5).to_a
#=>[[2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8],
# [5, 6, 7, 8, 9], [6, 7, 8, 9, "T"], [7, 8, 9, "T", "J"],
# [8, 9, "T", "J", "Q"], [9, "T", "J", "Q", "K"],
# ["T", "J", "Q", "K", "A"]]
def straight?(hand)
(hand.map {|c| CARDS.index(c)}.sort == [0,1,2,3,12]) ||
STRAIGHTS.include?(hand.sort {|a,b| CARDS.index(a) <=> CARDS.index(b)})
end
If we map each card to a value (9 is 9, "T" is 10, "J" is 11, etc.), then there are two facts that are true of all straights that we can use to solve our problem:
All straights have exactly five unique card values
The difference between the last and first cards' values is always 4
And so:
CARD_VALUES = {
2 => 2, 3 => 3, 4 => 4,
5 => 5, 6 => 6, 7 => 7,
8 => 8, 9 => 9, "T" => 10,
"J" => 11, "Q" => 12, "K" => 13,
"A" => 14
}
def is_straight?(hand)
hand_sorted = hand.map {|card| CARD_VALUES[card] }
.sort.uniq
hand_sorted.size == 5 &&
(hand_sorted.last - hand_sorted.first) == 4
end
This method (1) converts each card to its numeric value with map, then (2) sorts them, and then (3) throws out duplicates with uniq. To illustrate with various hands:
hand | 4 A T A 2 | 2 2 3 3 4 | 5 6 4 8 7 | 3 6 2 8 7
---------+--------------------+--------------------+--------------------+----------------
1. map | 4 14 10 14 2 | 2 2 3 3 4 | 5 6 4 8 7 | 3 6 2 8 7
2. sort | 2 4 10 14 14 | 2 2 3 3 4 | 4 5 6 7 8 | 2 3 6 7 8
3. uniq | 2 4 10 14 | 2 3 4 | 4 5 6 7 8 | 2 3 6 7 8
Alternatively...
I originally posted the following solution, which isn't bad, but is definitely more convoluted:
If the hand is sorted, this is easy. You can use Enumerable#each_cons to check each possible straight.
CARDS = [ 2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A" ]
hand = [ 4, 5, 6, 7, 8 ]
def is_straight?(hand)
CARDS.each_cons(5).any? do |straight|
hand == straight
end
end
if is_straight?(hand)
puts "Straight!"
else
puts "Not straight!"
end
# => Straight!
each_cons(5) returns each consecutive set of 5 items, so in the above example hand is first compared to [ 2, 3, 4, 5, 6 ], then [ 3, 4, 5, 6, 7 ], and then [ 4, 5, 6, 7, 8 ], which is a match, so any? returns true.
Note that this is not the most efficient solution, but unless you need to check many thousands of hands per second, this is more than adequately performant.
If your hands aren't sorted yet, you'll need to do that first. The simplest way to do that is create a Hash that maps cards to a numeric value (as above) and then use sort_by:
def sort_hand(hand)
hand.sort_by {|card| CARD_VALUES[card] }
end
hand = [ 4, "A", 2, "A", "T" ]
sort_hand(hand)
# => [ 2, 4, "T", "A", "A" ]
I did not want to participate, but I can’t keep silence looking at all these oversophisticated solutions around.
hand = [2, 5, 7, 'A', 'J'].map(&:to_s)
'23456789TJQKA' =~ hand.sort_by{|hc| '23456789TJQKA'.index(hc)}.join ||
'A23456789TJQK' =~ hand.sort_by{|hc| 'A23456789TJQK'.index(hc)}.join
In a not lame hardcoded manner:
suit = '23456789TJQKA'
suit =~ hand.sort_by{|hc| suit.index(hc)}.join ||
suit.rotate(-1) =~ hand.sort_by{|hc| suit.rotate(-1).index(hc)}.join
Generate list of valid hands:
valid_hands = cards[0..8].each_with_index.map{|b,i| cards[i..i+4]}
#=> [[2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8], [5, 6, 7, 8, 9], [6, 7, 8, 9, "T"], [7, 8, 9, "T", "J"], [8, 9, "T", "J", "Q"], [9, "T", "J", "Q", "K"], ["T", "J", "Q", "K", "A"]]
Once you have the list of all valid hands, you can now check if provided hand is among any? of them (valid ones) or not:
if valid_hands.any? { |h| (h - hand).empty? }
puts "Valid hand"
else
puts "Not Valid"
end
UPDATE
In-case 2, 3, 4, 5, "A", 2, 3, 4, "K", "A", 2, 3, "Q", "K", "A", 2, "J", "Q", "K", "A" are also considered as valid hands, calculate them as follows:
valid_hands = cards.each_with_index.map { |b,i| i < 9 ? cards[i..i+4] : cards[0..i-9] + cards[i..-1] }
# => [[2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8], [5, 6, 7, 8, 9], [6, 7, 8, 9, "T"], [7, 8, 9, "T", "J"], [8, 9, "T", "J", "Q"], [9, "T", "J", "Q", "K"], ["T", "J", "Q", "K", "A"], [2, "J", "Q", "K", "A"], [2, 3, "Q", "K", "A"], [2, 3, 4, "K", "A"], [2, 3, 4, 5, "A"]]
I recommend writing classes to represent a Card (and maybe Deck and Hand too). Aim for an interface like this:
deck = Deck.new.shuffle!
hand = Hand.new(deck.draw 5)
hand.straight?
#=>false
puts hand
8♣ 8♦ T♠ 2♦ 7♦
The encapsulation of functionality gives you readability and makes it easy to extend (i.e. with suits)
Here's a more simplistic version, implemented as a single Card class. I did add suits though.
class Card
include Enumerable #enables sorting
attr_accessor :value, :suit
#values = [2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A"]
#suits = ["♣","♦","♥","♠"]
def self.all
#values.product(#suits).map{|c| Card.new c}
end
def self.straight?(cards)
["A", *#values].each_cons(5).include?(cards.map(&:value))
end
def self.flush?(cards)
cards.map(&:suit).uniq.size == 1
end
def initialize(v)
#value, #suit = *v
end
def <=>(other) #for sorting
#values.index(value) <=> #values.index(other.value)
end
def to_s
"#{value}#{suit}"
end
end
This works as follows
deck = Card.all
puts deck
#=> 2♣ 2♦ 2♥ 2♠ 3♣ 3♦ 3♥ 3♠ 4♣ 4♦ 4♥ 4♠ 5♣ 5♦ 5♥ 5♠ 6♣ 6♦ 6♥ 6♠ 7♣ 7♦ 7♥ 7♠ 8♣ 8♦ 8♥ 8♠ 9♣ 9♦ 9♥ 9♠ T♣ T♦ T♥ T♠ J♣ J♦ J♥ J♠ Q♣ Q♦ Q♥ Q♠ K♣ K♦ K♥ K♠ A♣ A♦ A♥ A♠
hand = deck.sample 5
puts hand
#=> Q♥ 6♦ 2♣ T♠ Q♦
Card.straight?(hand)
#=>false
Step 0: Let's start with an empty class
class CardUtils
end
Step 1: Store values of card in Hash
Hash allows fast referencing of values of a card.
##card_values = {
'A' => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5,
6 => 6, 7 => 7, 8 => 8, 9 => 9, 'T' => 10,
'J' => 11, 'Q' => 12, 'K' => 13
}
Thus, you can reference the card value simply as below.
##card_values['A']
# => 1
##card_values[8]
# => 8
Step 2: Sort the hand
Apply sort! method to the hand with reference to the card values.
def self.sort(hand)
hand.sort {|x,y| ##card_values[x] <=> ##card_values[y]}
end
# => ["A", 2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K"]
Step 3: Function that tells whether two cards are consecutive
def self.is_consecutive(x, y)
val_x = ##card_values[x]
val_y = ##card_values[y]
val_x == val_y - 1 || val_x + 13 == val_y
end
# is_consecutive('A', 2)
# => true
# is_consecutive('K', 'A')
# => true
# is_consecutive('A', 3)
# => false
Step 4: Check for 'straight'
It could be done with simple iteration.
def self.has_straight(hand)
hand = sort(hand)
max_consecutive_count = 0
consecutive_count = 0
hand.each_with_index do |curr, i|
prev = hand[i - 1]
if is_consecutive(prev, curr) then
consecutive_count += 1
else
consecutive_count = 0
end
if consecutive_count > max_consecutive_count then
max_consecutive_count = consecutive_count
end
end
max_consecutive_count >= 5
end
# hand = [2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A"]
# CardUtils.has_straight(hand)
# => true
Final Result
class CardUtils
##card_values = {
'A' => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5,
6 => 6, 7 => 7, 8 => 8, 9 => 9, 'T' => 10,
'J' => 11, 'Q' => 12, 'K' => 13
}
def self.is_consecutive(x, y)
val_x = ##card_values[x]
val_y = ##card_values[y]
val_x == val_y - 1 || val_x + 13 == val_y
end
def self.sort(hand)
hand.sort {|x,y| ##card_values[x] <=> ##card_values[y]}
end
def self.has_straight(hand)
hand = sort(hand)
max_consecutive_count = 0
consecutive_count = 0
hand.each_with_index do |curr, i|
prev = hand[i - 1]
if is_consecutive(prev, curr) then
consecutive_count += 1
else
consecutive_count = 0
end
if consecutive_count > max_consecutive_count then
max_consecutive_count = consecutive_count
end
end
max_consecutive_count >= 5
end
end
This is how I would write it:
hand = [3,4,5,2,'A']
def is_straight(hand)
# No need to check further if we do not have 5 unique cards.
return false unless hand.uniq.size == 5
# Note the A at beginning AND end to count A as 1 or 14.
list_of_straights = 'A23456789TJQKA'.chars.each_cons(5)
sorted_hand = hand.map(&:to_s).sort
list_of_straights.any? do |straight|
straight.sort==sorted_hand
end
end
puts is_straight(hand) #=> true
Alternatively if you do not like all the sorting you could exchange the last part to:
hand_as_stings = hand.map(&:to_s)
list_of_straights.any? do |straight|
(straight-hand_as_stings).empty?
end

Ruby Array - Delete first 10 digits

I have an array in Ruby and I would like to delete the first 10 digits in the array.
array = [1, "a", 3, "b", 2, "c", 4, "d", 5, "a", 1, "z", 7, "e", 21, "q", 30, "a", 4, "t", 7, "m", 5, 1, 2, "q", "s", "l", 13, 46, 31]
It would ideally return
['a', 'b', 'c', 'd', 'a', 'z', 'e', 'q', 0, 'a', 4, t, 7, m, 5 , 1, 2, q, s, 1, 13, 46, 31]
By removing the first 10 digits (1,3,2,4,5,1,7,2,1,3).
Note that 21(2 and 1) and 30(3 and 0) both have 2 digits
Here's what I've tried
digits = array.join().scan(/\d/).first(10).map{|s|s.to_i}
=> [1,3,2,4,5,1,7,2,1,3]
elements = array - digits
This is what I got
["a", "b", "c", "d", "a", "z", "e", 21, "q", 30, "a", "t", "m", "q", "s", "l", 13, 46, 31]
Now it looks like it took the difference instead of subtracting.
I have no idea where to go from here. and now I'm lost. Any help is appreciated.
To delete 10 numbers:
10.times.each {array.delete_at(array.index(array.select{|i| i.is_a?(Integer)}.first))}
array
To delete 10 digits:
array = [1, "a", 3, "b", 2, "c", 4, "d", 5, "a", 1, "z", 7, "e", 21, "q", 30, "a", 4, "t", 7, "m", 5, 1, 2, "q", "s", "l", 13, 46, 31]
i = 10
while (i > 0) do
x = array.select{|item| item.is_a?(Integer)}.first
if x.to_s.length > i
y = array.index(x)
array[y] = x.to_s[0, (i-1)].to_i
else
array.delete_at(array.index(x))
end
i -= x.to_s.length
end
array
Unfortunately not a one-liner:
count = 10
array.each_with_object([]) { |e, a|
if e.is_a?(Integer) && count > 0
str = e.to_s # convert integer to string
del = str.slice!(0, count) # delete up to 'count' characters
count -= del.length # subtract number of actually deleted characters
a << str.to_i unless str.empty? # append remaining characters as integer if any
else
a << e
end
}
#=> ["a", "b", "c", "d", "a", "z", "e", "q", 0, "a", 4, "t", 7, "m", 5, 1, 2, "q", "s", "l", 13, 46, 31]
I would be inclined to do it like this.
Code
def doit(array, max_nbr_to_delete)
cnt = 0
array.map do |e|
if (e.is_a? Integer) && cnt < max_nbr_to_delete
cnt += e.to_s.size
if cnt <= max_nbr_to_delete
nil
else
e.to_s[cnt-max_nbr_to_delete..-1].to_i
end
else
e
end
end.compact
end
Examples
array = [ 1, "a", 3, "b", 2, "c", 4, "d", 5, "a", 1, "z", 7, "e", 21, "q",
30, "a", 4, "t", 7, "m", 5, 1, 2, "q", "s", "l", 13, 46, 31]
doit(array, 10)
#=> ["a", "b", "c", "d", "a", "z", "e", "q", 0, "a", 4,
# "t", 7, "m", 5, 1, 2, "q", "s", "l", 13, 46, 31]
doit(array, 100)
#=> ["a", "b", "c", "d", "a", "z", "e", "q", "a", "t", "m", "q", "s", "l"]
Explanation
Each element e of the array that is not an integer is mapped to e.
For each non-negative integer n having d digits, suppose cnt is the number of digits that map has already been removed from the string. There are three possibilities:
if cnt >= max_nbr_to_delete, no more digits are to be removed, so e (itself) is returned
if cnt + d <= max_nbr_to_delete all d digits of e are to be removed, which is done by mapping e to nil and subsequently removing nil elements
if cnt < max_nbr_to_delete and cnt + d > max_nbr_to_delete, e.to_s[cnt+d-max_nbr_to_delete..-1].to_i is returned (i.e. the first cnt+d-max_nbr_to_delete digits of e are removed).

Resources