Decode base45 string - ruby

We are trying to implement the verification of the new EU corona virus test/vaccination certificates, but can't get the base45 decoding working.
Specification is here: https://datatracker.ietf.org/doc/draft-faltstrom-base45/
We nearly finished our class, but we sometimes get wrong values back..
Target is this:
Encoding example 1: The string "AB" is the byte sequence [65 66].
The 16 bit value is 65 * 256 + 66 = 16706. 16706 equals 11 + 45 * 11
+ 45 * 45 * 8 so the sequence in base 45 is [11 11 8]. By looking up
these values in the Table 1 we get the encoded string "BB8".
Encoding example 2: The string "Hello!!" as ASCII is the byte
sequence [72 101 108 108 111 33 33]. If we look at each 16 bit
value, it is [18533 27756 28449 33]. Note the 33 for the last byte.
When looking at the values modulo 45, we get [[38 6 9] [36 31 13] [9
2 14] [33 0]] where the last byte is represented by two. By looking
up these values in the Table 1 we get the encoded string "%69
VD92EX0".
Encoding example 3: The string "base-45" as ASCII is the byte
sequence [98 97 115 101 45 52 53]. If we look at each 16 bit value,
it is [25185 29541 11572 53]. Note the 53 for the last byte. When
looking at the values modulo 45, we get [[30 19 12] [21 26 14] [7 32
5] [8 1]] where the last byte is represented by two. By looking up
these values in the Table 1 we get the encoded string "UJCLQE7W581".
Here is my current code, which produces wrong values:
class Base45
ALPHABET = {
"00" => "0",
"01" => "1",
"02" => "2",
"03" => "3",
"04" => "4",
"05" => "5",
"06" => "6",
"07" => "7",
"08" => "8",
"09" => "9",
"10" => "A",
"11" => "B",
"12" => "C",
"13" => "D",
"14" => "E",
"15" => "F",
"16" => "G",
"17" => "H",
"18" => "I",
"19" => "J",
"20" => "K",
"21" => "L",
"22" => "M",
"23" => "N",
"24" => "O",
"25" => "P",
"26" => "Q",
"27" => "R",
"28" => "S",
"29" => "T",
"30" => "U",
"31" => "V",
"32" => "W",
"33" => "X",
"34" => "Y",
"35" => "Z",
"36" => " ",
"37" => "$",
"38" => "%",
"39" => "*",
"40" => "+",
"41" => "-",
"42" => ".",
"43" => "/",
"44" => ":"
}.freeze
def self.encode_base45(text)
restsumme = text.unpack('S>*')
# not sure what this is doing, but without it, it works worse :D
restsumme << text.bytes[-1] if text.bytes.size > 2 && text.bytes[-1] < 256
bytearr = restsumme.map do |bytes|
arr = []
multiplier, rest = bytes.divmod(45**2)
arr << multiplier if multiplier > 0
multiplier, rest = rest.divmod(45)
arr << multiplier if multiplier > 0
arr << rest if rest > 0
arr.reverse
end
return bytearr.flatten.map{|a| ALPHABET[a.to_s.rjust(2, "0")]}.join
end
def self.decode_base45(text)
arr = text.split("").map do |char|
ALPHABET.invert[char]
end
textarr = arr.each_slice(3).to_a.map do |group|
subarr = group.map.with_index do |val, index|
val.to_i * (45**index)
end
ap subarr
subarr.sum
end
return textarr.pack("S>*") # returns wrong values
end
end
Results:
Base45.encode_base45("AB")
=> "BB8" # works
Base45.decode_base45("BB8")
=> "AB" # works
Base45.encode_base45("Hello!!")
=> "%69 VD92EX" # works
Base45.decode_base45("BB8")
=> "Hello!\x00!" # wrong \x00
Base45.encode_base45("base-45")
=> "UJCLQE7W581" # works
Base45.decode_base45("UJCLQE7W581")
=> "base-4\x005" # wrong \x00
Any hints appreciated :(

After struggling to get the other answers to work, I made my own method based on your question and this snippet. Abovementioned answers worked in most cases but not in all of them, especially when the string length mod 3 = 2.
class Base45
ALPHABET = {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
10 => "A",
11 => "B",
12 => "C",
13 => "D",
14 => "E",
15 => "F",
16 => "G",
17 => "H",
18 => "I",
19 => "J",
20 => "K",
21 => "L",
22 => "M",
23 => "N",
24 => "O",
25 => "P",
26 => "Q",
27 => "R",
28 => "S",
29 => "T",
30 => "U",
31 => "V",
32 => "W",
33 => "X",
34 => "Y",
35 => "Z",
36 => " ",
37 => "$",
38 => "%",
39 => "*",
40 => "+",
41 => "-",
42 => ".",
43 => "/",
44 => ":"
}.freeze
def self.decode_base45(text)
raise ArgumentError, "invalid base45 string" if text.size % 3 == 1
arr = text.split("").map do |char|
ALPHABET.invert[char]
end
arr.each_slice(3).to_a.map do |group|
if group.size == 3
x = group[0] + group[1] * 45 + group[2] * 45 * 45
raise ArgumentError, "invalid base45 string" if x > 0xFFFF
x.divmod(256)
else
x = group[0] + group[1] * 45
raise ArgumentError, "invalid base45 string" if x > 0xFF
x
end
end.flatten.pack("C*")
end
end

if you'd like a bodgy way of doing this:
return textarr.map{|x| x<256 ? [x].pack("C*") : [x].pack("n*") }.join
looking at this scheme, it feels like a weird way to encode, as we're working with numbers ... if it were me, I'd have started at the tail of the string and worked towards the head, but that's because we're using numbers.
anyway, the reason that my bodge works is that it treats small elements/numbers as 8-bit unsigned instead of 16-bit unsigned.
...
slightly more pleasing to the eye, but probably no better:
def self.decode_base45(text)
arr = text.split("").map do |char|
ALPHABET.invert[char]
end
textarr = arr.each_slice(3).to_a.map do |group|
subarr = group.map.with_index do |val, index|
val.to_i * (45**index)
end
ap subarr
subarr.sum.divmod(256)
end.flatten.reject(&:zero?)
return textarr.pack("C*") # returns wrong values
end

It might not be a proper solution to the problem here.
But adding textarr.pack("S>*").gsub(/\x00/, "") solved the problem for the given decoding examples.
Also it's really weird that your encode version didn't work well for me (had wrong results in two first examples).
Anyway, this thread led me to contribute a bit by making this as a gem.

Let's take QED8WEX0 and QED8WEX00 for example, you got [[26, 14, 13], [8, 32, 14], [33, 0]] and [[26, 14, 13], [8, 32, 14], [33, 0, 0]] respectively.
The problem here is their final form are the same: [26981, 29798, 33] and you can't determine whether 33 represents for 1 bytes or 2 bytes.
We can solve it by passing dynamic argument based on input length to Array#pack, for example:
[26981, 29798, 33].pack((input.length % 3).zero? ? 'n*' : "n#{input.length / 3}C")
Here is my implementation:
# frozen_string_literal: true
class Base45Lite
MAX_UINT18 = 2**16 - 1
SQUARED_45 = 45**2
MAPPING = [
*'0'..'9',
*'A'..'Z',
' ', '$', '%', '*', '+', '-', '.', '/', ':'
].map!.with_index { |x, i| [i, x] }.to_h.freeze
REVERSE_MAPPING = MAPPING.invert.freeze
def self.encode(input)
sequence = []
input.unpack('n*').map! do |uint16|
i, c = uint16.divmod(45)
i, d = i.divmod(45)
_, e = i.divmod(45)
sequence.push(c, d, e)
end
if input.bytesize.odd?
i, c = input.getbyte(-1).divmod(45)
_, d = i.divmod(45)
sequence.push(c, d)
end
sequence.map!{ MAPPING[_1] }.join
end
def self.decode(input)
input
.chars.map! { REVERSE_MAPPING[_1] }
.each_slice(3).map do |slice|
c, d, e = slice
sum = c + d * 45
sum += e * SQUARED_45 if e
sum
end
.pack((input.length % 3).zero? ? 'n*' : "n#{input.length / 3}C")
end
end
if __FILE__ == $PROGRAM_NAME
require 'minitest/autorun'
class Base45Test < Minitest::Test
parallelize_me!
def test_encode
assert_equal 'BB8', Base45Lite.encode('AB')
assert_equal '%69 VD92EX0', Base45Lite.encode('Hello!!')
assert_equal 'UJCLQE7W581', Base45Lite.encode('base-45')
end
def test_decode
assert_equal 'ietf!', Base45Lite.decode('QED8WEX0')
assert_equal 'AB', Base45Lite.decode('BB8')
assert_equal 'Hello!!', Base45Lite.decode('%69 VD92EX0')
assert_equal 'base-45', Base45Lite.decode('UJCLQE7W581')
end
end
end
You can find a more comprehensive implementation here https://gist.github.com/tonytonyjan/5eefdfbe7a79cd676e75c138466e921d
You can also install the Ruby gem https://rubygems.org/gems/base45_lite
Benchmark
Compare with https://github.com/wattswing/base45
require 'benchmark/ips'
require 'base45'
require_relative 'base45_lite'
message = 'base-45'
encoded = 'UJCLQE7W581'
Benchmark.ips do |x|
x.report('Base45Lite.encode'){ |n| n.times { Base45Lite.encode(message) } }
x.report('Base45.encode'){ |n| n.times { Base45.encode(message) } }
x.compare!
end
Benchmark.ips do |x|
x.report('Base45Lite.decode'){ |n| n.times { Base45Lite.decode(encoded) } }
x.report('Base45.decode'){ |n| n.times { Base45.decode(encoded) } }
x.compare!
end
Warming up --------------------------------------
Base45Lite.encode 37.721k i/100ms
Base45.encode 16.121k i/100ms
Calculating -------------------------------------
Base45Lite.encode 348.105k (± 6.7%) i/s - 1.735M in 5.008303s
Base45.encode 148.871k (± 7.1%) i/s - 741.566k in 5.008783s
Comparison:
Base45Lite.encode: 348105.0 i/s
Base45.encode: 148870.7 i/s - 2.34x (± 0.00) slower
Warming up --------------------------------------
Base45Lite.decode 25.909k i/100ms
Base45.decode 16.972k i/100ms
Calculating -------------------------------------
Base45Lite.decode 231.134k (± 6.9%) i/s - 1.166M in 5.068220s
Base45.decode 148.288k (± 6.5%) i/s - 746.768k in 5.057694s
Comparison:
Base45Lite.decode: 231134.5 i/s
Base45.decode: 148287.8 i/s - 1.56x (± 0.00) slower

Related

Replacing letters in string with their numeric position in the alphabet in ruby

I'm trying to make a method that, given a string, replaces every letter with its position in the alphabet.
If anything in the text isn't a letter, I want to ignore it and not return it.
"a" = 1, "b" = 2, etc.
Example
alphabet_position("The sunset sets at twelve o' clock.")
Should return "20 8 5 19 21 14 19 5 20 19 5 20 19 1 20 20 23 5 12 22 5 15 3 12 15 3 11" (as a string)
I tried this, but it didn't work:
def alphabet_position(text)
alph = ("a".."z").to_a
text = text.split(/./).map {|ch| if ch.in?(alph)
((alph.index[ch]).to_i+1).to_s
else
""
end
}.join(" ").strip
end
Thanks in advance!
First we can build the translation because Hash lookups are extremely fast:
# alternatively letter_to_number = ('a'..'z').each.with_index(1).to_h
letter_to_number = ('a'..'z').zip(1..26).to_h
#=> {"a"=>1, "b"=>2, "c"=>3, "d"=>4, "e"=>5, "f"=>6,
# "g"=>7, "h"=>8, "i"=>9, "j"=>10, "k"=>11, "l"=>12,
# "m"=>13, "n"=>14, "o"=>15, "p"=>16, "q"=>17, "r"=>18,
# "s"=>19, "t"=>20, "u"=>21, "v"=>22, "w"=>23, "x"=>24, "y"=>25, "z"=>26}
Then simply swap them out
# ruby >= 2.7
text.downcase.each_char.filter_map {|c| letter_to_number[c] }.join(' ')
# ruby < 2.7
text.downcase.each_char.map {|c| letter_to_number[c] }.compact.join(' ')
#=> "20 8 5 19 21 14 19 5 20 19 5 20 19 1 20 20 23 5 12 22 5 15 3 12 15 3 11"
def convert(str)
str.downcase.each_char.with_object('') do |c,s|
n = c.ord
if n.between?(97, 122)
s << ' ' unless s.empty?
s << (n-96).to_s
end
end
end
convert "Sunset was at twelve o'clock, somewhere, on some day"
#=> "19 21 14 19 5 20 23 1 19 1 20 20 23 5 12 22 5 15 3 12 15 3 11 19 15 13 5 23 8 5 18 5 15 14 19 15 13 5 4 1 25"
Lady and gentleman, start your engines!
def alphabet_position(text)
text.downcase.split('').map do |letter|
index = ('a'..'z').find_index(letter)
index + 1 if index
end.compact.join(' ')
end
def smnky_1_7(str)
letter_to_number = ('a'..'z').zip(1..26).to_h
str.downcase.each_char.filter_map {|c| letter_to_number[c] }.join(' ')
end
def smnky_pre_1_7(str)
letter_to_number = ('a'..'z').zip(1..26).to_h
str.downcase.each_char.map {|c| letter_to_number[c] }.compact.join(' ')
end
LETTER_TO_NUMBER = ('a'..'z').zip(1..26).to_h
def smnky_pre_w_constant(str)
str.downcase.each_char.map {|c| LETTER_TO_NUMBER[c] }.compact.join(' ')
end
def convert_with_arr(str)
str.downcase.each_char.with_object([]) do |c,arr|
n = c.ord
arr << n - 96 if n.between?(97, 122)
end.join(' ')
end
str = "Sunset was at twelve o'clock, somewhere, on some day"
require 'benchmark'
def run(m, str)
500.times { method(m).call(str) }
end
Benchmark.bm(19) do |x|
x.report("Cary") { run(:convert, str) }
x.report("Tatiana") { run(:alphabet_position, str) }
x.report("smnky 1.7") { run(:smnky_1_7, str) }
x.report("smnky pre 1.7") { run(:smnky_1_7, str) }
x.report("smnky pre w/constant") { run(:smnky_pre_w_constant, str) }
x.report("Cary with arr") { run(:convert_with_arr, str) }
end
user system total real
Cary 0.018610 0.000300 0.018910 ( 0.019135)
Tatiana 0.067738 0.001138 0.068876 ( 0.070317)
smnky 1.7 0.028659 0.001035 0.029694 ( 0.030583)
smnky pre 1.7 0.032050 0.001662 0.033712 ( 0.035089)
smnky pre w/constant 0.013705 0.000323 0.014028 ( 0.014139)
Cary with arr 0.016989 0.000538 0.017527 ( 0.017925)
In order to debug a piece of code, run it line by line. Pretty soon you'll find that:
text = "The sunset sets at twelve o' clock."
text.split(/./) #=> []
It does not do what you think it does.
I think you meant one of those:
text.split('') #=> ["T", "h", "e", " ", ...
text.chars #=> ["T", "h", "e", " ", ...
There are several more bugs but I'll leave those for you to figure out.
def alphabet_position(text)
text.downcase.split('').map do |letter|
index = ('a'..'z').find_index(letter)
index + 1 if index
end.compact.join(' ')
end
Following the answer of #Siim Liiser, also you can use scan method to get letters.
text = "The sunset sets at twelve o' clock."
irb(main):002:0> text.scan(/[a-z]/)
=> ["h", "e", "s", "u", "n", "s", "e", "t", "s", "e", "t", "s", "a", "t", "t", "w", "e", "l", "v", "e", "o", "c", "l", "o", "c", "k"]
As you can see, the letter T is uppercase and are missing, to solve this youy can use downcase before to call the method scan:
irb(main):004:0> text.downcase.scan(/[a-z]/)
=> ["t", "h", "e", "s", "u", "n", "s", "e", "t", "s", "e", "t", "s", "a", "t", "t", "w", "e", "l", "v", "e", "o", "c", "l", "o", "c", "k"]

Return min values of hash with duplicate values

For example, if I have the hash {"a" => 1, "b" => 2, "c" => 1}, I want ["a", "c"].
I can do hash.min_by{|k,v| v}, but that only returns the first match ("a"=>1).
How do I get it to recognize duplicates and return {"a"=> 1, "c"=> 1}?
That operation is a bit unusual for a hash, so it’s not very neat:
min_value = hash.values.min
min_pairs = hash.select { |k, v| v == min_value }
{"a" => 1, "b" => 2, "c" => 1}.group_by(&:last).min.last.map(&:first)
# => ["a", "c"]
or
{"a" => 1, "b" => 2, "c" => 1}.group_by(&:last).min.last.to_h.keys
# => ["a", "c"]
You can write FORTRAN in any language! :)
It has the advantage of only requiring 1 pass :
hash = {"a" => 1, "b" => 2, "c" => 1}
min = Float::INFINITY
values_for_min = []
hash.each do |key, value|
case value <=> min
when 0
values_for_min << key
when -1
min = value
values_for_min = [key]
end
end
p min
#=> 1
p values_for_min
#=> ["a", "c"]

Ruby How to convert string to integer without .to_i

Is there a way to output an integer given a string containing numbers between 0 and 9. For example, input is "219", output would be 219, and you can't use .to_i
You can use Kernel::Integer:
Integer("219")
#=> 219
Integer("21cat9")
# ArgumentError: invalid value for Integer(): "21cat9"
Sometimes this method is used as follows:
def convert_to_i(str)
begin
Integer(str)
rescue ArgumentError
nil
end
end
convert_to_i("219")
#=> 219
convert_to_i("21cat9")
#=> nil
convert_to_i("1_234")
#=> 1234
convert_to_i(" 12 ")
#=> 12
convert_to_i("0b11011") # binary representation
#=> 27
convert_to_i("054") # octal representation
#=> 44
convert_to_i("0xC") # hexidecimal representation
#=> 12
Some use an "inline rescue" (though it is less selective, as it rescues all exceptions):
def convert_to_i(str)
Integer(str) rescue nil
end
There are similar Kernel methods to convert a string to a float or rational.
def str_to_int(string)
digit_hash = {"0" => 0, "1" => 1, "2" => 2, "3" => 3, "4" => 4, "5" => 5, "6" => 6, "7" => 7, "8" => 8, "9" => 9}
total = 0
num_array = string.split("").reverse
num_array.length.times do |i|
num_value = digit_hash[num_array[i]]
num_value_base_ten = num_value * (10**i)
total += num_value_base_ten
end
return total
end
puts str_to_int("119") # => 119

convert phone number into words

So I figured out that I can count through the hash, the problem is that for 7 and 9 I have four values. I have tried several other things with no luck. Can someone help understand what else I could do to get the values I want out of the hash. I realize that I can match the numbers with the key, but I am confused how to get the values to permute.
letters = {"1" => ["1", "1", "1"],
"2" => ["a", "b", "c"],
"3" => ["d", "e", "f"],
"4" => ["g", "h", "i"],
"5" => ["j", "k", "l"],
"6" => ["m", "n", "o"],
"7" => ["p", "q", "r", "s"],
"8" => ["t", "u", "v"],
"9" => ["w", "x", "y", "z"]}
phone_number = gets.chomp.to_s
words = []
word = []
numbers = phone_number.chomp.chars
count0 = 0
while count0 < 3
count1 = 0
while count1 < 3
count2 = 0
while count2 < 3
count3 = 0
while count3 < 3
count4 = 0
while count4 < 3
count5 = 0
while count5 < 3
count6 = 0
while count6 < 3
word[0] = letters[numbers[0]][count0]
word[1] = letters[numbers[1]][count1]
word[2] = letters[numbers[2]][count2]
word[3] = letters[numbers[3]][count3]
word[4] = letters[numbers[4]][count4]
word[5] = letters[numbers[5]][count5]
word[6] = letters[numbers[6]][count6]
words << word.join
count6 += 1
end
count5 += 1
end
count4 += 1
end
count3 += 1
end
count2 += 1
end
count1 += 1
end
count0 += 1
end
puts words
Edit:
I want to a seven digit number and print out all possible letter combinations. I am a beginner so I want to understand with the things I know now. I want to try and do this with if statements please.
numbers = phone_number.chomp.chars
if letters.key?(numbers[0])
if letters.key?(numbers[1])
if letters.key?(numbers[2])
if letters.key?(numbers[3])
if letters.key?(numbers[4])
if letters.key?(numbers[5])
if letters.key?(numbers[6])
end
end
end
end
end
end
end
I understand how to grab a value from a matching key, but don't get how I can hold the first value while going through the rest, if that makes any sense.
product is the function you are looking for, the following works with any number of digits:
digits = '27'
keys = digits.chars.map{|digit|letters[digit]}
p keys.shift.product(*keys).map(&:join) #=> ["ap", "aq", "ar", "as", "bp", "bq", "br", "bs", "cp", "cq", "cr", "cs"]
This prints all possible words for a variable-sized phone number:
letters = {"1" => ["1"],
"2" => ["a", "b", "c"],
"3" => ["d", "e", "f"],
"4" => ["g", "h", "i"],
"5" => ["j", "k", "l"],
"6" => ["m", "n", "o"],
"7" => ["p", "q", "r", "s"],
"8" => ["t", "u", "v"],
"9" => ["w", "x", "y", "z"]}
digits = gets.chomp.split ''
# Total number of combinations
n = digits.inject(1) { |a,b| a * letters[b].size }
words = []
0.upto n-1 do |q|
word = []
digits.reverse.each do |digit|
q, r = q.divmod letters[digit].size
word.unshift letters[digit][r]
end
words << word.join
end
puts words
For example, if the input is 67, then there are 12 combinations:
mp mq mr ms np nq nr ns op oq or os
Edit: I don't see a way to make use of the 7 if statements as you have written, but perhaps this is closer to the kind of answer you are looking for:
words = []
letters[digits[0]].each do |c0|
letters[digits[1]].each do |c1|
letters[digits[2]].each do |c2|
letters[digits[3]].each do |c3|
letters[digits[4]].each do |c4|
letters[digits[5]].each do |c5|
letters[digits[6]].each do |c6|
words << [c0,c1,c2,c3,c4,c5,c6].join
end
end
end
end
end
end
end
puts words
A good exercise would be to re-write this in a way that can work for phone numbers of any length, not just 7. Again, this is only for instructional purposes. In practice, one would use Array's product method as in hirolau's answer.
LETTERS = {"1" => ["1", "1", "1"],
"2" => ["a", "b", "c"],
"3" => ["d", "e", "f"],
"4" => ["g", "h", "i"],
"5" => ["j", "k", "l"],
"6" => ["m", "n", "o"],
"7" => ["p", "q", "r", "s"],
"8" => ["t", "u", "v"],
"9" => ["w", "x", "y", "z"]}
def convert_to_phone_number(string)
string.each_char.with_object([]) { |x, arr| LETTERS.each { |k,v| (arr.push k; break) if v.include?(x) }}.join
end
convert_to_phone_number "foobar"
#=> "366227"
i think so it is issue of cache memory
u need to change like below
LETTERS = {"1" => ["1", "1", "1"],
"2" => ["a", "b", "c"],
"3" => ["d", "e", "f"],
"4" => ["g", "h", "i"],
"5" => ["j", "k", "l"],
"6" => ["m", "n", "o"],
"7" => ["p", "q", "r", "s"],
"8" => ["t", "u", "v"],
"9" => ["w", "x", "y", "z"]}
def convert_to_phone_number(string)
string.each_char.with_object([]) { |x, arr| LETTERS.each { |k,v| (arr.push k; break) if v.include?(x) }}.join
end
convert_to_phone_number "foobar"

ruby: how to find non-unique elements in array and print each with number of occurrences?

I have
a = ["a", "d", "c", "b", "b", "c", "c"]
and need to print something like (sorted descending by number of occurrences):
c:3
b:2
I understand first part (finding NON-unique) is:
b = a.select{ |e| a.count(e) > 1 }
=> ["c", "b", "b", "c", "c"]
or
puts b.select{|e, c| [e, a.count(e)] }.uniq
c
b
How to output each non-unique with number of occurrences sorted backwards?
puts a.uniq.
map { | e | [a.count(e), e] }.
select { | c, _ | c > 1 }.
sort.reverse.
map { | c, e | "#{e}:#{c}" }
The group_by method is used for this often:
a.group_by{ |i| i }
{
"a" => [
[0] "a"
],
"d" => [
[0] "d"
],
"c" => [
[0] "c",
[1] "c",
[2] "c"
],
"b" => [
[0] "b",
[1] "b"
]
}
I like:
a.group_by{ |i| i }.each_with_object({}) { |(k,v), h| h[k] = v.size }
{
"a" => 1,
"d" => 1,
"c" => 3,
"b" => 2
}
Or:
Hash[a.group_by{ |i| i }.map{ |k,v| [k, v.size] }]
{
"a" => 1,
"d" => 1,
"c" => 3,
"b" => 2
}
One of those might scratch your itch. From there you can reduce the result using a little test:
Hash[a.group_by{ |i| i }.map{ |k,v| v.size > 1 && [k, v.size] }]
{
"c" => 3,
"b" => 2
}
If you just want to print the information use:
puts a.group_by{ |i| i }.map{ |k,v| "#{k}: #{v.size}" }
a: 1
d: 1
c: 3
b: 2
From Ruby 2.7, you can utilise Enumerable#tally and numbered block arguments:
a = ["a", "d", "c", "b", "b", "c", "c"]
puts a.tally.filter { _2 > 1 }.sort_by { -_2 }.map &:first
Here, Enumerable#tally returns a hash like { 'a' => 1, 'b' => 2, ... }, which you then have to filter and sort. After sorting, the hash would've collapsed to a nested array, e.g. [['b', 2], ...]. The last step is to take the first argument of each array element, using &:first.
How about:
a.sort.chunk{|x| a.count(x)}.sort.reverse.each do |n, v|
puts "#{v[0]}:#{n}" if n > 1
end
I personally like this solution:
a.inject({}) {|hash, val| hash[val] ||= 0; hash[val] += 1; hash}.
reject{|key, value| value == 1}.sort.reverse.
each_pair{|k,v| puts("#{k}:#{v}")}
a.reduce(Hash.new(0)) { |memo,x| memo[x] += 1; memo } # Frequency count.
.select { |_,count| count > 1 } # Choose non-unique items.
.sort_by { |x| -x[1] } # Sort by number of occurrences descending.
# => [["c", 3], ["b", 2]]
Also:
a.group_by{|x|x}.map{|k,v|[k,v.size]}.select{|x|x[1]>1}.sort_by{|x|-x[1]}
# => [["c", 3], ["b", 2]]
This will give you a hash with element => occurrences:
b.reduce(Hash.new(0)) do |hash, element|
hash[element] += 1
hash
end
puts a.uniq.
map { |e| a.count(e) > 1 ? [e, a.count(e)] : nil }.compact.
sort { |a, b| b.last <=> a.last }

Resources