0

This is a simple code to parse a simple string in ruby

str = "Amdh#34HB!x"

length = str.length
upper = str.scan(/[A-Z]/).count                         #count upper chars
lower = str.scan(/[a-z]/).count                         #count lower chars
digit = str.scan(/[0-9]/).count                         #count digits
special = str.scan(/[^a-z0-9]/i).count                  #count special chars
result = "Length : " + length.to_s + " Upper : " + upper.to_s + " Lower : " + lower.to_s + " Digit : " + digit .to_s + " Special : " + special.to_s

The result is given as "Length : 11 Upper : 3 Lower : 4 Digit : 2 Special : 2"

I want to do the same things to an array of strings

array = ["Amdh#34HB!x", "AzErtY45", "#1A3bhk2"]

and so I can know the details like above of each element of the array

Example :

array[0] = "Length : 11 Upper : 3 Lower : 4 Digit : 2 Special : 2"
array[1] = "Length : 8 Upper : 3 Lower : 3 Digit : 2 Special : 0"
array[2] = "Length : 8 Upper : 1 Lower : 3 Digit : 3 Special : 1"

....

I know the answer seems simple by using each method but didn't find the right way to do it.

The code above is not optimised, if there is any better suggestion, you are welcome!

0

6 Answers 6

2

You need only make a single pass through the string to obtain the needed counts.

def obtain_counts(str)
  str.each_char.with_object(Hash.new(0)) do |c,h|
    h[case(c)
      when /[[:upper:]]/ then :upper
      when /[[:lower:]]/ then :lower 
      when /[[:digit:]]/ then :digit
      else :special
      end] += 1
  end
end

def construct_array(arr)
  arr.map! { |str|
    "Length : #{str.length} Upper : %d Lower : %d Digit : %d Special : %d" %
      obtain_counts(str).values_at(:upper, :lower, :digit, :special) }
end

array = ["Amdh#34HB!x", "AzErtY45", "#1A3bhk2"]

construct_array(array)
  #=> ["Length : 11 Upper : 3 Lower : 4 Digit : 2 Special : 2",
  #    "Length : 8 Upper : 3 Lower : 3 Digit : 2 Special : 0",
  #    "Length : 8 Upper : 1 Lower : 3 Digit : 3 Special : 1"] 
array
  #=> ["Length : 11 Upper : 3 Lower : 4 Digit : 2 Special : 2",
  #    "Length : 8 Upper : 3 Lower : 3 Digit : 2 Special : 0",
  #    "Length : 8 Upper : 1 Lower : 3 Digit : 3 Special : 1"] 

Note that

["Amdh#34HB!x", "AzErtY45", "#1A3bhk2"].map { |str| obtain_counts(str) }
  #=> [{:upper=>3, :lower=>4, :special=>2, :digit=>2},
  #    {:upper=>3, :lower=>3, :digit=>2},
  #    {:special=>1, :digit=>3, :upper=>1, :lower=>3}]

Notice that the second hash in this array has no key :special (because the second string contained no special characters). That explains why, in obtain_counts, we need Hash.new(0) (empty hash with default 0), rather than simply {}.

Sign up to request clarification or add additional context in comments.

Comments

2

You may use simple map to do this:

array = ["Amdh#34HB!x", "AzErtY45", "#1A3bhk2"]

result = array.map do |str|
  length = str.length
  upper = str.scan(/[A-Z]/).count                         #count upper chars
  lower = str.scan(/[a-z]/).count                         #count lower chars
  digit = str.scan(/[0-9]/).count                         #count digits
  special = str.scan(/[^a-z0-9]/i).count                  #count special chars

  {length: length, upper: upper, lower: lower, digit: digit, special: special}
end


[117] pry(main)> result
=> [{:length=>11, :upper=>3, :lower=>4, :digit=>2, :special=>2},
 {:length=>8, :upper=>3, :lower=>3, :digit=>2, :special=>0},
 {:length=>8, :upper=>1, :lower=>3, :digit=>3, :special=>1}]

1 Comment

It would also be a good idea to move that block of code into a new method.
1

I guess you want to do this:

array = ["Amdh#34HB!x", "AzErtY45", "#1A3bhk2"]
result = []

array.each do |str|
  length = str.length
  upper = str.scan(/[A-Z]/).count                         #count upper chars                                                                                                                                       
  lower = str.scan(/[a-z]/).count                         #count lower chars                                                                                                                                       
  digit = str.scan(/[0-9]/).count                         #count digits                                                                                                                                            
  special = str.scan(/[^a-z0-9]/i).count                  #count special chars                                                                                                                                     
  result << "Length : " + length.to_s + " Upper : " + upper.to_s + " Lower : " + lower.to_s + " Digit : " + digit .to_s + " Special : " + special.to_s
end

puts result

1 Comment

Use map instead of each and creating a new array. (As shown by the other answer.)
1

try this solution:

def string_check(str)
  result = str.scan(/([A-Z])|([a-z])|([0-9])|([^a-z0-9])/).inject([0,0,0,0]) do 
    |sum,arr| sum.map.with_index{|e,i| e+(arr[i].nil? ? 0: 1) if !arr.nil?}
  end
  "Length : #{str.size} Upper : #{result[0]} Lower : #{result[1]} Digit : #{result[2]} Special : #{result[3]}"
end

array = ["Amdh#34HB!x", "AzErtY45", "#1A3bhk2"]

array.each {|s| puts string_check(s)}

outputs:

Length : 11 Upper : 3 Lower : 4 Digit : 2 Special : 2  
Length : 8 Upper : 3 Lower : 3 Digit : 2 Special : 0  
Length : 8 Upper : 1 Lower : 3 Digit : 3 Special : 1

1 Comment

Why not simply [/[A-Z]/, /[a-z]/, /[0-9]/, /[^a-z0-9]/].map{ |r| str.scan(r).length }
1

More DRY solution:

array = ["Amdh#34HB!x", "AzErtY45", "#1A3bhk2"]
formats = { Upper: /[A-Z]/, 
            Lower: /[a-z]/, 
            Digit: /[0-9]/, 
            Special: /[^a-z0-9]/i }

array.map do |e|
  "Length: #{e.length}, " + 
    formats.map {|k, v| "#{k}: #{e.scan(v).count}" }.join(', ')
end

#=> ["Length: 11, Upper: 3, Lower: 4, Digit: 2, Special: 2", 
#    "Length: 8, Upper: 3, Lower: 3, Digit: 2, Special: 0", 
#    "Length: 8, Upper: 1, Lower: 3, Digit: 3, Special: 1"]

Comments

1

Here's a start to help you move into a more OO Ruby script:

class Foo
  attr_reader :str, :str_len, :upper, :lower, :digit, :punctuation
  def initialize(str)
    @str = str
    @str_len = str.length
    @upper, @lower, @digit, @punctuation = %w[upper lower digit punct].map { |re| str.scan(/[[:#{re}:]]/).count }
  end

  def to_s
    ('"%s": ' % str) +
    [:str_len, :upper, :lower, :digit, :punctuation].map { |s|
      '%s: %s' % [s.to_s.upcase, instance_variable_get("@#{s}")]
    }.join(' ')
  end
end

array = ["Amdh#34HB!x", "AzErtY45", "#1A3bhk2"].map { |s| Foo.new(s) }
puts array.map(&:to_s)

Which, when run, outputs:

"Amdh#34HB!x": STR_LEN: 11 UPPER: 3 LOWER: 4 DIGIT: 2 PUNCTUATION: 2
"AzErtY45": STR_LEN: 8 UPPER: 3 LOWER: 3 DIGIT: 2 PUNCTUATION: 0
"#1A3bhk2": STR_LEN: 8 UPPER: 1 LOWER: 3 DIGIT: 3 PUNCTUATION: 1

The regular expression classes like [[:upper:]] are POSIX definitions, which help relieve some of the visual noise of a traditional expression's classes. See the Regexp documentation for more information.

It can be DRYer but that's an exercise for the student. You should be able to coerce this into something closer to what you want.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.