Thread.critical not working in ruby 2.5.0 - ruby
I'm using ruby2.5.0 and I have the below function as part of my script. When I run it I get the below error:
ensure in get_database_connection': undefined methodcritical=' for
Thread:Class (NoMethodError)
I understand the for ruby1.9.0 and above, Thread.critical is no longer supported so how can I edit my function to make it run under ruby2.5.0 ?
Thanks.
# Geodict
# Copyright (C) 2010 Pete Warden <pete#petewarden.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
require 'rubygems'
require 'postgres'
require 'set'
# Some hackiness to include the library script, even if invoked from another directory
require File.join(File.expand_path(File.dirname(__FILE__)), 'dstk_config')
# Global holder for the database connections
$connections = {}
# The main entry point. This function takes an unstructured text string and returns a list of all the
# fragments it could identify as locations, together with lat/lon positions
def find_locations_in_text(text)
current_index = text.length-1
result = []
$tokenized_words = {}
setup_countries_cache()
setup_regions_cache()
# This loop goes through the text string in *reverse* order. Since locations in English are typically
# described with the broadest category last, preceded by more and more specific designations towards
# the beginning, it simplifies things to walk the string in that direction too
while current_index>=0 do
current_word, pulled_index, ignored_skipped = pull_word_from_end(text, current_index)
lower_word = current_word.downcase
could_be_country = $countries_cache.has_key?(lower_word)
could_be_region = $regions_cache.has_key?(lower_word)
if not could_be_country and not could_be_region
current_index = pulled_index
next
end
# This holds the results of the match function for the final element of the sequence. This lets us
# optimize out repeated calls to see if the end of the current string is a country for example
match_cache = {}
token_result = nil
# These 'token sequences' describe patterns of discrete location elements that we'll look for.
$token_sequences.each() do |token_sequence|
# The sequences are specified in the order they'll occur in the text, but since we're walking
# backwards we need to reverse them and go through the sequence in that order too
token_sequence = token_sequence.reverse
# Now go through the sequence and see if we can match up all the tokens in it with parts of
# the string
token_result = nil
token_index = current_index
token_sequence.each_with_index do |token_name, token_position|
# The token definition describes how to recognize part of a string as a match. Typical
# tokens include country, city and region names
token_definition = $token_definitions[token_name]
match_function = token_definition[:match_function]
# This logic optimizes out repeated calls to the same match function
if token_position == 0 and match_cache.has_key?(token_name)
token_result = match_cache[token_name]
else
# The meat of the algorithm, checks the ending of the current string against the
# token testing function, eg seeing if it matches a country name
token_result = send(match_function, text, token_index, token_result)
if token_position == 0
match_cache[token_name] = token_result
end
end
if !token_result
# The string doesn't match this token, so the sequence as a whole isn't a match
break
else
# The current token did match, so move backwards through the string to the start of
# the matched portion, and see if the preceding words match the next required token
token_index = token_result[:found_tokens][0][:start_index]-1
end
end
# We got through the whole sequence and all the tokens match, so we have a winner!
if token_result
current_word, current_index, end_skipped = pull_word_from_end(text, current_index)
break
end
end
if !token_result
# None of the sequences matched, so back up a word and start over again
ignored_word, current_index, end_skipped = pull_word_from_end(text, current_index)
else
# We found a matching sequence, so add the information to the result
result.push(token_result)
found_tokens = token_result[:found_tokens]
current_index = found_tokens[0][:start_index]-1
end
end
# Reverse the result so it's in the order that the locations occured in the text
result.reverse!
return result
end
# Functions that look at a small portion of the text, and try to identify any location identifiers
# Caches the countries and regions tables in memory
$countries_cache = {}
$is_countries_cache_setup = false
def setup_countries_cache()
if $is_countries_cache_setup then return end
select = 'SELECT * FROM countries'
hashes = select_as_hashes(select, DSTKConfig::DATABASE)
hashes.each do |hash|
last_word = hash['last_word'].downcase
if !$countries_cache.has_key?(last_word)
$countries_cache[last_word] = []
end
$countries_cache[last_word].push(hash)
end
$is_countries_cache_setup = true
end
$regions_cache = {}
$is_regions_cache_setup = false
def setup_regions_cache()
if $is_regions_cache_setup then return end
select = 'SELECT * FROM regions'
hashes = select_as_hashes(select, DSTKConfig::DATABASE)
hashes.each do |hash|
last_word = hash['last_word'].downcase
if !$regions_cache.has_key?(last_word)
$regions_cache[last_word] = []
end
$regions_cache[last_word].push(hash)
end
$is_regions_cache_setup = true
end
# Translates a two-letter country code into a readable name
def get_country_name_from_code(country_code)
if !country_code then return nil end
setup_countries_cache()
result = country_code
$countries_cache.each do |last_word, countries|
countries.each do |row|
if row['country_code'] and row['country_code'].downcase == country_code.downcase
result = row['country']
end
end
end
result
end
# Matches the current fragment against our database of countries
def is_country(text, text_starting_index, previous_result)
current_word = ''
current_index = text_starting_index
pulled_word_count = 0
found_row = nil
# Walk backwards through the current fragment, pulling out words and seeing if they match
# the country names we know about
while pulled_word_count < DSTKConfig::WORD_MAX do
pulled_word, current_index, end_skipped = pull_word_from_end(text, current_index)
pulled_word_count += 1
if current_word == ''
# This is the first time through, so the full word is just the one we pulled
current_word = pulled_word
# Make a note of the real end of the word, ignoring any trailing whitespace
word_end_index = (text_starting_index-end_skipped)
# We've indexed the locations by the word they end with, so find all of them
# that have the current word as a suffix
last_word = pulled_word.downcase
if !$countries_cache.has_key?(last_word)
break
end
candidate_dicts = $countries_cache[last_word]
name_map = {}
candidate_dicts.each do |candidate_dict|
name = candidate_dict['country'].downcase
name_map[name] = candidate_dict
end
else
current_word = pulled_word+' '+current_word
end
# This happens if we've walked backwards all the way to the start of the string
if current_word == ''
return nil
end
# If the first letter of the name is lower case, then it can't be the start of a country
# Somewhat arbitrary, but for my purposes it's better to miss some ambiguous ones like this
# than to pull in erroneous words as countries (eg thinking the 'uk' in .co.uk is a country)
if current_word[0].chr =~ /[a-z]/
next
end
name_key = current_word.downcase
if name_map.has_key?(name_key)
found_row = name_map[name_key]
end
if found_row
# We've found a valid country name
break
end
if current_index < 0
# We've walked back to the start of the string
break
end
end
if !found_row
# We've walked backwards through the current words, and haven't found a good country match
return nil
end
# Were there any tokens found already in the sequence? Unlikely with countries, but for
# consistency's sake I'm leaving the logic in
if !previous_result
current_result = {
:found_tokens => [],
}
else
current_result = previous_result
end
country_code = found_row['country_code']
lat = found_row['lat']
lon = found_row['lon']
# Prepend all the information we've found out about this location to the start of the :found_tokens
# array in the result
current_result[:found_tokens].unshift({
:type => :COUNTRY,
:code => country_code,
:lat => lat,
:lon => lon,
:matched_string => current_word,
:start_index => (current_index+1),
:end_index => word_end_index
})
return current_result
end
# Looks through our database of 2 million towns and cities around the world to locate any that match the
# words at the end of the current text fragment
def is_city(text, text_starting_index, previous_result)
# If we're part of a sequence, then use any country or region information to narrow down our search
country_code = nil
region_code = nil
if previous_result
found_tokens = previous_result[:found_tokens]
found_tokens.each do |found_token|
type = found_token[:type]
if type == :COUNTRY:
country_code = found_token[:code]
elsif type == :REGION:
region_code = found_token[:code]
end
end
end
current_word = ''
current_index = text_starting_index
pulled_word_count = 0
found_row = nil
while pulled_word_count < DSTKConfig::WORD_MAX do
pulled_word, current_index, end_skipped = pull_word_from_end(text, current_index)
pulled_word_count += 1
if current_word == ''
current_word = pulled_word
word_end_index = (text_starting_index-end_skipped)
select = "SELECT * FROM cities WHERE last_word='"+pulled_word.downcase+"'"
if country_code
select += " AND country='"+country_code.downcase+"'"
end
if region_code
select += " AND region_code='"+region_code.upcase.strip+"'"
end
# There may be multiple cities with the same name, so pick the one with the largest population
select += ' ORDER BY population;'
hashes = select_as_hashes(select, DSTKConfig::DATABASE)
name_map = {}
hashes.each do |hash|
name = hash['city'].downcase
name_map[name] = hash
end
else
current_word = pulled_word+' '+current_word
end
if current_word == ''
return nil
end
if current_word[0].chr =~ /[a-z]/
next
end
name_key = current_word.downcase
if name_map.has_key?(name_key)
found_row = name_map[name_key]
end
if found_row
break
end
if current_index < 0
break
end
end
if !found_row
return nil
end
if !previous_result
current_result = {
:found_tokens => [],
}
else
current_result = previous_result
end
lat = found_row['lat']
lon = found_row['lon']
country_code = found_row['country'].downcase
current_result[:found_tokens].unshift( {
:type => :CITY,
:lat => lat,
:lon => lon,
:country_code => country_code,
:matched_string => current_word,
:start_index => (current_index+1),
:end_index => word_end_index
})
return current_result
end
# This looks for sub-regions within countries. At the moment the only values in the database are for US states
def is_region(text, text_starting_index, previous_result)
# Narrow down the search by country, if we already have it
country_code = nil
if previous_result
found_tokens = previous_result[:found_tokens]
found_tokens.each do |found_token|
type = found_token[:type]
if type == :COUNTRY
country_code = found_token[:code]
end
end
end
current_word = ''
current_index = text_starting_index
pulled_word_count = 0
found_row = nil
while pulled_word_count < DSTKConfig::WORD_MAX do
pulled_word, current_index, end_skipped = pull_word_from_end(text, current_index)
pulled_word_count += 1
if current_word == ''
current_word = pulled_word
word_end_index = (text_starting_index-end_skipped)
last_word = pulled_word.downcase
if !$regions_cache.has_key?(last_word)
break
end
all_candidate_dicts = $regions_cache[last_word]
if country_code
candidate_dicts = []
all_candidate_dicts.each do |possible_dict|
candidate_country = possible_dict['country_code']
if candidate_country.downcase() == country_code.downcase():
candidate_dicts << possible_dict
end
end
else
candidate_dicts = all_candidate_dicts
end
name_map = {}
candidate_dicts.each do |candidate_dict|
name = candidate_dict['region'].downcase
name_map[name] = candidate_dict
end
else
current_word = pulled_word+' '+current_word
end
if current_word == ''
return nil
end
if current_word[0].chr =~ /[a-z]/
next
end
name_key = current_word.downcase
if name_map.has_key?(name_key)
found_row = name_map[name_key]
end
if found_row
break
end
if current_index < 0
break
end
end
if !found_row
return nil
end
if !previous_result
current_result = {
:found_tokens => [],
}
else
current_result = previous_result
end
region_code = found_row['region_code']
lat = found_row['lat']
lon = found_row['lon']
country_code = found_row['country_code'].downcase
current_result[:found_tokens].unshift( {
:type => :REGION,
:code => region_code,
:lat => lat,
:lon => lon,
:country_code => country_code,
:matched_string => current_word,
:start_index => (current_index+1),
:end_index=> word_end_index
})
return current_result
end
# A special case - used to look for 'at' or 'in' before a possible location word. This helps me be more certain
# that it really is a location in this context. Think 'the New York Times' vs 'in New York' - with the latter
# fragment we can be pretty sure it's talking about a location
def is_location_word(text, text_starting_index, previous_result)
current_index = text_starting_index
current_word, current_index, end_skipped = pull_word_from_end(text, current_index)
word_end_index = (text_starting_index-end_skipped)
if current_word == ''
return nil
end
current_word.downcase!
if !DSTKConfig::LOCATION_WORDS.has_key?(current_word)
return nil
end
return previous_result
end
def is_postal_code(text, text_starting_index, previous_result)
# Narrow down the search by country, if we already have it
country_code = nil
if previous_result
found_tokens = previous_result[:found_tokens]
found_tokens.each do |found_token|
type = found_token[:type]
if type == :COUNTRY
country_code = found_token[:code]
end
end
end
current_word = ''
current_index = text_starting_index
pulled_word_count = 0
found_rows = nil
while pulled_word_count < DSTKConfig::WORD_MAX do
pulled_word, current_index, end_skipped = pull_word_from_end(text, current_index)
pulled_word_count += 1
if current_word == ''
current_word = pulled_word
word_end_index = (text_starting_index-end_skipped)
last_word = pulled_word.downcase
select = "SELECT * FROM postal_codes"
select += " WHERE last_word='"+pulled_word.downcase+"'"
select += " OR last_word='"+pulled_word.upcase+"'"
if country_code
select += " AND country_code='"+country_code.upcase+"'"
end
candidate_dicts = select_as_hashes(select, DSTKConfig::DATABASE)
name_map = {}
candidate_dicts.each do |candidate_dict|
name = candidate_dict['postal_code'].downcase
if !name_map[name] then name_map[name] = [] end
name_map[name] << candidate_dict
end
else
current_word = pulled_word+' '+current_word
end
if current_word == ''
return nil
end
if current_word[0].chr =~ /[a-z]/
next
end
name_key = current_word.downcase
if name_map.has_key?(name_key)
found_rows = name_map[name_key]
end
if found_rows
break
end
if current_index < 0
break
end
end
if !found_rows
return nil
end
# Confirm the postal code against the country suffix
found_row = nil
if country_code
found_rows.each do |row|
if row['country_code'] == country_code
found_row = row
break
end
end
end
if !found_row
return nil
end
# Also pull in the prefixed region, if there is one
region_result = is_region(text, current_index, nil)
if region_result
region_token = region_result[:found_tokens][0]
region_code = region_token[:code]
if found_row['region_code'] == region_code
current_index = region_token[:start_index]-1
current_word = region_token[:matched_string] + ' ' + current_word
end
end
if !found_row
return nil
end
if !previous_result
current_result = {
:found_tokens => [],
}
else
current_result = previous_result
end
region_code = found_row['region_code']
lat = found_row['lat']
lon = found_row['lon']
country_code = found_row['country_code'].downcase
region_code = found_row['region_code'].downcase
postal_code = found_row['postal_code'].downcase
current_result[:found_tokens].unshift( {
:type => :POSTAL_CODE,
:code => postal_code,
:lat => lat,
:lon => lon,
:region_code => region_code,
:country_code => country_code,
:matched_string => current_word,
:start_index => (current_index+1),
:end_index=> word_end_index
})
return current_result
end
# Characters to ignore when pulling out words
WHITESPACE = " \t'\",.-/\n\r<>!?".split(//).to_set
$tokenized_words = {}
# Walks backwards through the text from the end, pulling out a single unbroken sequence of non-whitespace
# characters, trimming any whitespace off the end
def pull_word_from_end(text, index, use_cache=true)
if use_cache and $tokenized_words.has_key?(index)
return $tokenized_words[index]
end
found_word = ''
current_index = index
end_skipped = 0
while current_index>=0 do
current_char = text[current_index].chr
current_index -= 1
if WHITESPACE.include?(current_char)
if found_word == ''
end_skipped += 1
next
else
current_index += 1
break
end
end
found_word << current_char
end
# reverse the result (since we're appending for efficiency's sake)
found_word.reverse!
result = [found_word, current_index, end_skipped]
$tokenized_words[index] = result
return result
end
# Converts the result of an SQL fetch into an associative dictionary, rather than a numerically indexed list
def get_hash_from_row(fields, row)
d = {}
fields.each_with_index do |field, index|
value = row[index]
d[field] = value
end
return d
end
# Returns the most specific token from the array
def get_most_specific_token(tokens)
if !tokens then return nil end
result = nil
result_priority = nil
tokens.each do |token|
priority = $token_priorities[token[:type]]
if !result or result_priority > priority
result = token
result_priority = priority
end
end
result
end
# Returns the results of the SQL select statement as associative arrays/hashes
def select_as_hashes(select, database_name)
begin
conn = get_database_connection(database_name)
Thread.critical = true
res = conn.exec('BEGIN')
res.clear
res = conn.exec('DECLARE myportal CURSOR FOR '+select)
res.clear
res = conn.exec('FETCH ALL in myportal')
fields = res.fields
rows = res.result
res = conn.exec('CLOSE myportal')
res = conn.exec('END')
result = []
rows.each do |row|
hash = get_hash_from_row(fields, row)
result.push(hash)
end
rescue PGError
if conn
printf(STDERR, conn.error)
else
$stderr.puts 'select_as_hashes() - no connection for ' + database_name
end
if conn
conn.close
end
$connections[database_name] = nil
exit(1)
ensure
Thread.critical = false
end
return result
end
def get_database_connection(database_name)
begin
Thread.critical = true
if !$connections[database_name]
$connections[database_name] = PGconn.connect(DSTKConfig::HOST,
DSTKConfig::PORT,
'',
'',
database_name,
DSTKConfig::USER,
DSTKConfig::PASSWORD)
end
ensure
Thread.critical = false
end
if !$connections[database_name]
$stderr.puts "get_database_connection('#{database_name}') - Couldn't open connection"
end
$connections[database_name]
end
# Types of locations we'll be looking for
$token_definitions = {
:COUNTRY => {
:match_function => :is_country
},
:CITY => {
:match_function => :is_city
},
:REGION => {
:match_function => :is_region
},
:LOCATION_WORD => {
:match_function => :is_location_word
},
:POSTAL_CODE => {
:match_function => :is_postal_code
}
}
# Particular sequences of those location words that give us more confidence they're actually describing
# a place in the text, and aren't coincidental names (eg 'New York Times')
$token_sequences = [
[ :POSTAL_CODE, :REGION, :COUNTRY ],
[ :REGION, :POSTAL_CODE, :COUNTRY ],
[ :POSTAL_CODE, :CITY, :COUNTRY ],
[ :POSTAL_CODE, :COUNTRY ],
[ :CITY, :COUNTRY ],
[ :CITY, :REGION ],
[ :REGION, :COUNTRY ],
[ :COUNTRY ],
[ :LOCATION_WORD, :REGION ], # Regions and cities are too common as words to use without additional evidence
[ :LOCATION_WORD, :CITY ]
]
# Location identifiers in order of decreasing specificity
$token_priorities = {
:POSTAL_CODE => 0,
:CITY => 1,
:REGION => 2,
:COUNTRY => 3,
}
if __FILE__ == $0
require 'json'
test_text = <<-TEXT
Spain
Italy
Bulgaria
Foofofofof
New Zealand
Barcelona, Spain
Wellington New Zealand
I've been working on the railroad, all the live-long day! The quick brown fox jumped over the lazy dog in Alabama
I'm mentioning Los Angeles here, but without California or CA right after it, it won't be detected. If I talk about living in Wisconsin on the other hand, that 'in' gives the algorithm extra evidence it's actually a location.
It should still pick up more qualified names like Amman Jordan or Atlanta, Georgia though!
Dallas, TX or New York, NY
It should now pick up Queensland, Australia, or even NSW, Australia!
Postal codes like QLD 4002, Australia, QC H3W, Canada, 2608 Lillehammer, Norway, or CA 94117, USA are supported too.
TEXT
puts "Analyzing '#{test_text}'"
puts "Found locations:"
locations = find_locations_in_text(test_text)
locations.each_with_index do |location_info, index|
found_tokens = location_info[:found_tokens]
location = get_most_specific_token(found_tokens)
match_start_index = found_tokens[0][:start_index]
match_end_index = found_tokens[found_tokens.length-1][:end_index]
matched_string = test_text[match_start_index..match_end_index]
result = {
'type' => location[:type],
'name' => location[:matched_string],
'latitude' => location[:lat].to_s,
'longitude' => location[:lon].to_s,
'start_index' => location[:start_index].to_s,
'end_index' => location[:end_index].to_s,
'matched_string' => matched_string,
'country' => location[:country_code],
'code' => location[:code],
}
puts result.to_json
end
end
You can either remove the call, or ask first before trying:
Thread.respond_to(:critical=) and Thread.critical = true
That being said, since Thread.critical= was removed in Ruby 1.9 it's pretty safe to trash that code entirely. Anyone running Ruby 1.8.x is living dangerously.
Unless you have a specific requirement to support 1.8.x, you'll have to delete the calls and use an alternative.
The purpose of critical= is to prevent pre-emption of the thread by another. That's a really heavy-handed way to synchronize threads, and dangerous enough that Ruby pulled support for it lest that start to become more pervasive.
What you probably want is a Mutex if you need to lock a resource. There's no obviously shared resources here unless get_database_connection returns one. It doesn't seem to as the connection is closed on error.
This code is full of some seriously suspect things, like using $connections, a global variable, and hard-exiting the whole process on failure. You may want to do a more thorough investigation as to what the purpose of the critical lock was in the first place.
Related
Ruby If statement refactor
I'm coming back to Ruby after a long time away. I've written the following code which is functional: def generate_address_record(data) address = FM[:fap_address].build do |a| if data['Line 1'].blank? a.unstructured_address.line1 = nil else a.unstructured_address.line1 = data['Line 1'] end if data['Line 2'].blank? a.unstructured_address.line2 = nil else a.unstructured_address.line2 = data['Line 2'] end if data['Line 3'].blank? a.unstructured_address.line3 = nil else a.unstructured_address.line3 = data['Line 3'] end if data['Line 4'].blank? a.unstructured_address.line4 = nil else a.unstructured_address.line4 = data['Line 4'] end if data['Line 5'].blank? a.unstructured_address.line5 = nil else a.unstructured_address.line5 = data['Line 5'] end if data['Postcode'].blank? a.unstructured_address.postcode = nil else a.unstructured_address.postcode = data['Postcode'] end end end Is there a way this can be re-written in a 'nicer' way in one loop so that I don't need all of these individual if statements. Any advice would be greatly appreciated.
If data contains only expected values, you can convert key to the method name. def generate_address_record(data) address = FM[:fap_address].build do |a| data.each do |key, value| name = key.gsub(/[[:space:]]/, '').downcase a.unstructured_address.public_send("#{name}=", value.presence) end end end Be careful(or don't use this approach) when hash keys are coming from outside of the application or other uncontrolled environment.
Yup, you can use #presence (I'm assuming you're using Rails): a.unstructured_address.line1 = data['Line 1'].presence #presence behaviour: ''.presence # => nil nil.presence # => nil 'a'.presence # => "a" false.presence # => nil
I propose this combination of the various already posted solutions because I think it has a good balance between shortness and readability: def generate_address_record(data) address = FM[:fap_address].build do |a| a.unstructured_address.line1 = data['Line 1'].presence a.unstructured_address.line2 = data['Line 2'].presence a.unstructured_address.line3 = data['Line 3'].presence a.unstructured_address.line4 = data['Line 4'].presence a.unstructured_address.line5 = data['Line 5'].presence a.unstructured_address.postcode = data['Postcode'].presence end end
One pattern I find usefull is to make a hash and then iterate over it: def generate_address_record(data) address = FM[:fap_address].build do |a| { "Line 1" => :line1, "Line 2" => :line2, "Line 3" => :line3, "Line 4" => :line4, "Line 5" => :line5, "Postcode" => :postcode }.each do |key, accessor| if data[key].blank? a.unstructured_address.send(accessor) = nil else a.unstructured_address.send(:"#{accessor}=") = data[key] end end end end You can also use this with presence as mrzasa shared
Is there any different way to write below code by initializing hash's key with a default value?
Write a method that returns the no of various lowercase, uppercase, digits and special characters used in the string. Make use of Ranges. Input = "heLLo Every1" I am making using of ranges and case method in solution provided. Solution: class String def character_count uppercase_count = 0 lowercase_count = 0 digit_count = 0 uppercase_range = Range.new('A', 'Z') lowercase_range = Range.new('a', 'z') digit_range = Range.new('0', '9') special_character_count = 0 each_char do |item| case item when uppercase_range uppercase_count += 1 when lowercase_range lowercase_count += 1 when digit_range digit_count += 1 else special_character_count += 1 end end [lowercase_count, uppercase_count, digit_count, special_character_count] end end if ARGV.empty? puts 'Please provide an input' else string = ARGV[0] count_array = string.character_count puts "Lowercase characters = #{count_array[0]}" puts "Uppercase characters = #{count_array[1]}" puts "Numeric characters = #{count_array[2]}" puts "Special characters = #{count_array[3]}" end Code is working.
Yes class String def character_count counters = Hash.new(0) each_char do |item| case item when 'A'..'Z' counters[:uppercase] += 1 when 'a'..'z' counters[:lowercase] += 1 when '0'..'9' counters[:digit] += 1 else counters[:special] += 1 end end counters.values_at(:uppercase, :lowercase, :digit, :special) end end if ARGV.empty? puts 'Please provide an input' else string = ARGV[0] uppercase, lowercase, digit, special = string.character_count puts "Lowercase characters = #{lowercase}" puts "Uppercase characters = #{uppercase}" puts "Numeric characters = #{digit}" puts "Special characters = #{special}" end
You can instead use regex in better way as following, type = { special: /[^0-9A-Za-z]/, numeric: /[0-9]/, uppercase: /[A-Z]/, lowercase: /[a-z]/ } 'Hello World'.scan(type[:special]).count # => 1 'Hello World'.scan(type[:numeric]).count # => 0 'Hello World'.scan(type[:uppercase]).count # => 2 'Hello World'.scan(type[:lowercase]).count # => 8
Other option. First, map your ranges into an Hash: mapping = { upper: ('A'..'Z'), lower: ('a'..'z'), digits: ('0'..'9'), specials: nil } Then initialize the recipient Hash to default 0: res = Hash.new(0) Finally, map the chars of the input: input = "heLLo Every1" input.chars.each { |e| res[(mapping.find { |k, v| v.to_a.include? e } || [:specials]).first ] += 1 } res #=> {:upper=>3, :lower=>7, :digits=>1, :specials=>1}
str = "Agent 007 was on the trail of a member of SPECTRE" str.each_char.with_object(Hash.new(0)) do |c,h| h[ case c when /\d/ then :digit when /\p{Lu}/ then :uppercase when /\p{Ll}/ then :downcase else :special end ] += 1 end end #=> {:uppercase=>8, :downcase=>28, :special=>10, :digit=>3}
How can I parse a string into a hash?
I am trying to parse a string into a hash. str = "Notifications[0].Open=1 Notifications[0].Body.Message[0]=3455 Notifications[0].Body.Message[1]=2524 Notifications[0].Body.Message[2]=2544 Notifications[0].Body.Message[3]=2452 Notifications[0].Body.Error[0]=2455 Notifications[0].Body.Currency=EUR Notifications[0].Published=true" The result should look similar to this: pairs = { 'Open' = 1, 'Published' => true 'Body' => { 'Message' => [3455, 2524, 2544, 2452], 'Error' => [2455], 'Currency' => 'EUR', } } Maybe someone can help on how I can make it. The only way I can think as for now is regexp.
something like this with regexp: require 'pp' str = "Notifications[0].Open=1 Notifications[0].Body.Message[0]=3455 Notifications[0].Body.Message[1]=2524 Notifications[0].Body.Message[2]=2544 Notifications[0].Body.Message[3]=2452 Notifications[0].Body.Error[0]=2455 Notifications[0].Body.Currency=EUR Notifications[0].Published=true" pairs = {} pairs['Body'] = {} values = [] str.scan(/Body\W+(.+)/).flatten.each do |line| key = line[/\A\w+/] value = line[/\w+\z/] if line[/\A\w+\[\d+\]/] || key == 'Error' values = [] unless pairs['Body'][key] values << value value = values end pairs['Body'][key] = value end str.scan(/\[0\]\.(?!Body.).*/).each do |line| key = line[/(?!\A)\.(\w+)/, 1] value = line[/\w+\z/] if line[/\A\w+\[\d+\]/] values = [] unless pairs[key] values << value value = values end pairs[key] = value end PP.pp pairs - {"Body"=> {"Message"=>["3455", "2524", "2544", "2452"], "Error"=>["2455"], "Currency"=>"EUR"}, "Open"=>"1", "Published"=>"true"}
Here it is. This code should work with any structure. def parse(path, value, hash) key, rest = path.split('.', 2) if rest.nil? hash[key] = value else hash[key] ||= {} parse(rest, value, hash[key]) end end def conv_to_array(hash) if hash.is_a?(Hash) hash.each do |key, value| hash[key] = if value.is_a?(Hash) && value.keys.all? { |k| k !~ /\D/ } arr = [] value.each do |k, v| arr[k.to_i] = conv_to_array(v) end arr else conv_to_array(value) end end hash else if hash !~ /\D/ hash.to_i elsif hash == 'true' true elsif hash == 'false' false else hash end end end str = "Notifications[0].Open=1 Notifications[0].Body.Message[0]=3455 Notifications[0].Body.Message[1]=2524 Notifications[0].Body.Message[2]=2544 Notifications[0].Body.Message[3]=2452 Notifications[0].Body.Error[0]=2455 Notifications[0].Body.Currency=EUR Notifications[0].Published=true" str = str.tr('[', '.').tr(']', '') hash = {} str.split(' ').each do |chunk| path, value = chunk.split('=') parse(path.strip, value.strip, hash) end hash = conv_to_array(hash) hash['Notifications'][0] # => {"Open"=>1, "Body"=>{"Message"=>[3455, 2524, 2544, 2452], "Error"=>[2455], "Currency"=>"EUR"}, "Published"=>true}
How can I find acronym in a text?
My project reads many files (these files have title text and sections) and should find the title of the files that contain an acronym. This is my docs class: class Doc def initialize(id, secciones) #id, #secciones = id, secciones end def to_s result = "" + #id.to_s + "\n" + #secciones.to_s return result end def tiene_acronimo(acr) puts "a ver si tiene acronimos el docu.." tiene_acronimo = false secciones.each do |seccion| if seccion.tiene_acronimo(acr) tiene_acronimo = true end end return tiene_acronimo end attr_accessor :id attr_accessor :secciones end And this my sections class: class Section def initialize () #title = "" #text = "" end def tiene_acronimo(acr) return title.include?(acr) || text.include?(acr) end end And this my method: def test() results = Array.new puts "Dame el acronimo" acr = gets documentos_cientificos.each do |d| if d.tiene_acronimo(acr) results << d end end The method gets an acronym and should find all documents that contain it. The method inclue? [sic] ingores the upcase and returns true if the docs contain any substring like the acronym. For example: Multiple sclerosis (**MS**), also known as # => `true` Presenting signs and sympto**ms** # => `false` (but `include?` returns `true`) How I can find an acronym more easily?
You could use some regex with the match function. The following regex will find a match if the content contains the FULL word provided. It will ignore substrings, and it will be case sensitive. arc = "MS" title = "Multiple sclerosis (MS), also known as" text = "Presenting signs and symptoms" title.match(/\b#{Regexp.escape(acr)}\b/) # => #<MatchData "MS"> text.match(/\b#{Regexp.escape(acr)}\b/) # => nil or equivalently title.match(/\b#{Regexp.escape(acr)}\b/).to_a.size > 0 # => true text.match(/\b#{Regexp.escape(acr)}\b/).to_a.size > 0 # => false ...so you could redefine your function as: def tiene_acronimo(acr) regex_to_match = /\b#{Regexp.escape(acr)}\b/ has_acr = false if (title.match(regex_to_match)) || (text.match(regex_to_match)) has_acr = true end return has_acr end
Parse CSV Data with Ruby
I am trying to return a specific cell value based on two criteria. The logic: If ClientID = 1 and BranchID = 1, puts SurveyID Using Ruby 1.9.3, I want to basically look through an excel file and for two specific values located within the ClientID and BranchID column, return the corresponding value in the SurveyID column. This is what I have so far, which I found during my online searches. It seemed promising, but no luck: require 'csv' # Load file csv_fname = 'FS_Email_Test.csv' # Key is the column to check, value is what to match search_criteria = { 'ClientID' => '1', 'BranchID' => '1' } options = { :headers => :first_row, :converters => [ :numeric ] } # Save `matches` and a copy of the `headers` matches = nil headers = nil # Iterate through the `csv` file and locate where # data matches the options. CSV.open( csv_fname, "r", options ) do |csv| matches = csv.find_all do |row| match = true search_criteria.keys.each do |key| match = match && ( row[key] == search_criteria[key] ) end match end headers = csv.headers end # Once matches are found, we print the results # for a specific row. The row `row[8]` is # tied specifically to a notes field. matches.each do |row| row = row[1] puts row end I know the last bit of code following matches.each do |row| is invalid, but I left it in in hopes that it will make sense to someone else. How can I write puts surveyID if ClientID == 1 & BranchID == 1?
You were very close indeed. Your only error was setting the values of the search_criteria hash to strings '1' instead of numbers. Since you have converters: :numeric in there the find_all was comparing 1 to '1' and getting false. You could just change that and you're done. Alternatively this should work for you. The key is the line Hash[row].select { |k,v| search_criteria[k] } == search_criteria Hash[row] converts the row into a hash instead of an array of arrays. Select generates a new hash that has only those elements that appear in search_criteria. Then just compare the two hashes to see if they're the same. require 'csv' # Load file csv_fname = 'FS_Email_Test.csv' # Key is the column to check, value is what to match search_criteria = { 'ClientID' => 1, 'BranchID' => 1, } options = { headers: :first_row, converters: :numeric, } # Save `matches` and a copy of the `headers` matches = nil headers = nil # Iterate through the `csv` file and locate where # data matches the options. CSV.open(csv_fname, 'r', options) do |csv| matches = csv.find_all do |row| Hash[row].select { |k,v| search_criteria[k] } == search_criteria end headers = csv.headers end p headers # Once matches are found, we print the results # for a specific row. The row `row[8]` is # tied specifically to a notes field. matches.each { |row| puts row['surveyID'] }
Possibly... require 'csv' b_headers = false client_id_col = 0 branch_id_col = 0 survey_id_col = 0 CSV.open('FS_Email_Test.csv') do |file| file.find_all do |row| if b_headers == false then client_id_col = row.index("ClientID") branch_id_col = row.index("BranchID") survey_id_col = row.index("SurveyID") b_headers = true if branch_id_col.nil? || client_id_col.nil? || survey_id_col.nil? then puts "Invalid csv file - Missing one of these columns (or no headers):\nClientID\nBranchID\nSurveyID" break end else puts row[survey_id_col] if row[branch_id_col] == "1" && row[client_id_col] == "1" end end end