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.
I have a deeply nested Ruby hash which I need to transform into another hash. A Hash might have 0 or more children. Here's the input hash:
"{'id' => 'apple', 'children' => [{'id' => 'ipad', 'children' => [{'id' => 'ipadmini'}]},{'id' => 'ipadmini'}]}"
output
[{"ipad"=>{"id"=>"ipad", "paths"=>[[{"id"=>"apple"}, {"id"=>"ipad"}]]}},
{"ipadmini"=>{"id"=>"ipadmini", "paths"=>[[{"id"=>"ipad"}, {"id"=>"ipadmini"}], [{"id"=>"apple"}, {"id"=>"ipad"}, {"id"=>"ipadmini"}]]}}, {"apple"=>{"id"=>"apple", "paths"=>[[{"id"=>"apple"}]]}}]
my code:
def construct_concept(concept)
h = {}
c = Hash[*concept.to_a.first]
c['paths'] = [[Hash[*concept.to_a.first]]]
h[concept['id']] = c
h
end
def parent_child_concepts(concepts)
pc = {}
pc[:parent] = Hash[*concepts.first]
pc[:children] = concepts.values_at('children').flatten.map {|child| parent_child_concepts(child)} || []
pc
end
def add_parent_child_paths(parent_hash,children_array)
h = {}
parent_hash.each do |parent_key,parent_value|
h[parent_key] = parent_value
children_array.each do |child|
child.each do |k,v|
h[k] = v
h[k]['paths'].map {|path| path.unshift({'id' => parent_key})}
end
end
end
h
end
def build_concept_data(concepts)
#concept {"id"=>"apple", "children"=>[{"id"=>"ipad"}]}
parsed_concepts = parent_child_concepts(concepts)
parent = construct_concept(parsed_concepts[:parent])
children = parsed_concepts[:children].each_with_object([]) {|child,accu| accu << construct_concept(child)}
concept_paths_data = add_parent_child_paths(parent,children)
end
def build_data(node, ctx = [], result = [])
id = node['id']
children = node['children']
branch = result.find{|h| h.has_key?(id)}
if branch.nil?
result.push({
id => {
'id' => id,
'paths' => []
}
})
end
branch = result.find{|h| h.has_key?(id)}
branch[id]['paths'].unshift(
[ctx, id].flatten.map do |ctx_id|
{
'id' => ctx_id
}
end
)
if children.is_a?(Array)
children.each do |node|
new_ctx = ctx.dup
new_ctx.push(id)
nesting(node, new_ctx, result)
end
end
result
end
Working on a project and trying to turn this method (I Have some more similar methods like that in my project) into a more dynamic and concise way
Data from image
def proficiency_parser(stored_data, name, race, year, title, percentage)
if stored_data.has_key?(name)
if stored_data[name].has_key?(race)
if stored_data[name][race].has_key?(year)
stored_data[name][race][year][title] = percentage
else
stored_data[name][race][year] = {title => percentage}
end
else
stored_data[name][race] = {year => {title => percentage}}
end
else
stored_data[name] = {race => {year => {title => percentage}}}
end
end
so essentially this method through my data to identify whether it meets so of those specification showing in the code, essentially I just don't want to use this amount of "elses" and "Ifs" if at all possible.
Data
stored_data
# => {"COLORADO"=>{3=>{2008=>{:math=>0.697}}}}
name
# => "COLORADO"
race
# => 3
year
# => 2008
title
# => :math
percentage
# => 0.697
Take a look at Hash#dig which is included in Ruby versions 2.3.0 or newer.
To summarize:
hash_1 = { a: { a: { a: "b" } } }
hash_2 = { c: { c: { c: "d" } } }
hash_1.dig(:a, :a, :a) # returns "b"
hash_2.dig(:a, :a, :a) # returns nil
So you could say if hash_1.dig(:a, :a) instead of
if hash_1[:a]
if hash_1[:a][:a]
# etc
There's also another way to do it, which is to rescue your NoMethod [] errors.
Here's an example of that:
if hash_1[:a][:a][:a] rescue false
puts "the key exists"
else
puts "the key doesnt exist"
end
You can use some recursive call
Input data
stored_data = {}
name = 'COLORADO'
race = 3
year = 2008
title = :math
percentage = 0.697
Methods
def proficiency_parser(stored_data, name, race, year, title, percentage)
parser(stored_data, name, {race => {year => {title => percentage}}})
end
def parser(data, key, value)
data[key] ? value.each { |k, v| parser(data[key], k, v) } : data[key] = value
end
call
proficiency_parser(stored_data, name, race, year, title, percentage)
p stored_data
# => {"COLORADO"=>{3=>{2008=>{:math=>0.697}}}}
I hope this helps
I have this program I wrote to automate a task I have at work. This is my first attempt at programming, with zero experience or training so forgive any silly mistakes. (I removed the website and my username/password)
require "rubygems"
require "selenium-webdriver"
require "nokogiri"
browser = Selenium::WebDriver.for :firefox
wait = Selenium::WebDriver::Wait.new(:timeout => 15)
#loads to unassigned results page
browser.get "-----------------"
p browser.title
browser.find_element(name: "user[username]").send_keys "--------"
browser.find_element(name: "user[password]").send_keys "--------"
browser.find_element(name: "commit").click
p browser.title
browser.find_element(class_name: "status_notification").click
begin
browser.find_element(:xpath => ".//*[#id='sub_nav_content']/table/tbody/tr[2]/td[3]/a").click
table = wait.until {
element = browser.find_element(id: "possible_matched")
element if element.displayed?
}
if table
puts "Table Found"
else
puts "Table Error"
end
#creates an 2D array containing patient name, admit date and prints to screen
names = browser.find_elements(:xpath => ".//*[#id='possible_matched']/table/tbody/tr/td[1]")
name_array = []
names.each { |name| name_array << name.text}
admits = browser.find_elements(:xpath => ".//*[#id='possible_matched']/table/tbody/tr/td[5]")
admit_array = []
admits.each { |date| admit_array << date.text }
name_admit_array = name_array.zip(admit_array)
name_admit_array.each do |name, date|
puts "#{name}: #{date}"
end
#finds the location of the sub-array containing patient name and collection associated admit date
patient_name = browser.find_element(:xpath => ".//*[#id='dialog-modal-cancel-hl7-preview']/table/tbody/tr[2]/td[1]").text
collected_date = browser.find_element(:xpath => ".//*[#id='dialog-modal-cancel-hl7-preview']/table/tbody/tr[2]/td[4]").text
mo, da, yr = collected_date.split('/').map(&:to_i)
cd = [yr, mo, da]
bl = name_admit_array.each_with_index.select { |(name, date), i|
m, d, y = date.split('/').map(&:to_i)
dt = [y, m, d]
name.downcase == patient_name.downcase and (dt <=> cd)<0
}.map {|x, i| i }
# presses the button associated with the correct sub-array
blf = name_admit_array.values_at(*bl)
if bl.any?
bf = blf.rindex(blf.max) + 2
browser.find_element(:xpath => ".//*[#id='possible_matched']/table/tbody/tr[#{bf}]/td[6]/div/a").click
else
browser.find_element(:xpath => "html/body/div[6]/div[1]/a/span").click
end
end while bl.any?
puts "no name :("
So it runs the loop until there is nothing found in the array bl. What I want to do is have this loop run again but with the next link on the list of links. So at the beginning of the loop it should do browser.find_element(:xpath => ".//*[#id='sub_nav_content']/table/tbody/tr[3]/td[3]/a").clickinstead of browser.find_element(:xpath => ".//*[#id='sub_nav_content']/table/tbody/tr[2]/td[3]/a").click. Then is should run the rest of the loop in the same way. I want it to continue to increment tr[] each time the loop runs into br.any? => false.
you can create an array of xpaths, and run this code for each of them:
[
".//*[#id='sub_nav_content']/table/tbody/tr[3]/td[3]/a",
".//*[#id='sub_nav_content']/table/tbody/tr[2]/td[3]/a"
].each do |path|
begin
browser.find_element(:xpath => path).click
// etc....
end while bl.any?
end
I believe that more changes need to adjust your code reusable, depending on your needs
I've been trying to use Jekyll categories in a hierarchical fashion, i.e.
A: ['class', 'topic', 'foo']
AA: ['class', 'topic', 'foo', 'bar']
AB: ['class', 'topic', 'foo', 'baz']
AAA: ['class', 'topic', 'foo', 'bar', 'qux']
I'm trying to create a listing of all immediate subdirectories programmatically. That is, on a page with categories (A), I wish to be able to list the posts with categories (AA) and (AB), but not (AAA). Is this possible with Jekyll's vanilla structure, or should I consider using a plugin?
You need to use a plugin.
I've managed to make most of what you describe happen, but I'm not using categories at all, just representing a directory tree.
require 'digest/md5'
require 'open-uri'
module Jekyll
# Add accessor for directory
class Page
attr_reader :dir
end
class NavTree < Liquid::Tag
def render(context)
site = context.registers[:site]
#page_url = context.environments.first["page"]["url"]
#folder_weights = site.data['folder_weights']
#folder_icons = site.data['folder_icons']["icons"]
#nodes = {}
tree = {}
sorted_tree = {}
site.pages.each do |page|
# exclude all pages that are hidden in front-matter
if page.data["navigation"]["show"] != false
path = page.url
path = path.index('/')==0 ? path[1..-1] : path
#nodes[path] = page.data
end
end
#let's sort the pages by weight
array = []
#nodes.each do |path, data|
array.push(:path => path, :weight => data["weight"], :title => data["title"])
end
sorted_nodes = array.sort_by {|h| [-(h[:weight]||0), h[:path] ]}
sorted_nodes.each do |node|
current = tree
node[:path].split("/").inject("") do |sub_path,dir|
sub_path = File.join(sub_path, dir)
current[sub_path] ||= {}
current = current[sub_path]
sub_path
end
end
tree.each do |base, subtree|
folder_weight = #folder_weights[base]? #folder_weights[base] : 0
tree[base] = {"weight" => folder_weight, "subtree" => subtree}
end
tree_array = []
tree.each do |key, value|
tree_array.push(:base => key, :weight => value["weight"], :subtree => value["subtree"])
end
sorted_tree = tree_array.sort_by {|node| [ -(node[:weight]), node[:base] ]}
puts "generating nav tree for #{#page_url}"
files_first_traverse "", sorted_tree, 0
end
def files_first_traverse(prefix, nodes = [], depth=0)
output = ""
if depth == 0
id = 'id="nav-menu"'
end
output += "#{prefix}<ul #{id} class=\"nav nav-list\">"
nodes.each do |node|
base = node[:base]
subtree = node[:subtree]
name = base[1..-1]
if name.index('.') != nil
icon_name = #nodes[name]["icon"]
name = #nodes[name]["title"]
end
li_class = ""
if base == #page_url
li_class = "active"
if icon_name
icon_name = icon_name + " icon-white"
end
end
icon_html = "<span class=\"#{icon_name}\"></span>" unless icon_name.nil?
output += "#{prefix}<li class=#{li_class}>#{icon_html}#{name}</li>" if subtree.empty?
end
nodes.each do |node|
base = node[:base]
subtree = node[:subtree]
next if subtree.empty?
href = base
name = base[1..-1]
if name.index('.') != nil
is_parent = false
name = #nodes[name]["title"]
else
is_parent = true
href = base + '/index.html'
if name.index('/')
name = name[name.rindex('/')+1..-1]
end
end
name.gsub!(/_/,' ')
li_class = ""
if #page_url.index(base)
list_class = "collapsibleListOpen"
else
list_class = "collapsibleListClosed"
end
if href == #page_url
li_class = "active"
end
if is_parent
id = Digest::MD5.hexdigest(base)
icon_name = #folder_icons[base]
icon_html = icon_name.nil? ? "" : "<span class=\"#{icon_name}\"></span>"
li = "<li id=\"node-#{id}\" class=\"parent #{list_class}\"><div class=\"subtree-name\">#{icon_html}#{name}</div>"
else
icon_name = #nodes[name]["icon"]
if icon_name && li_class=="active"
icon_name = icon_name + " icon-white"
end
icon_html = icon_name.nil? ? "<i class=\"#{icon_name}\"></i>" : ""
li = "<li class=\"#{li_class}\">#{icon_html}#{name}</li>"
end
output += "#{prefix} #{li}"
subtree_array = []
subtree.each do |base, subtree|
subtree_array.push(:base => base, :subtree => subtree)
end
depth = depth + 1
output += files_first_traverse(prefix + ' ', subtree_array, depth)
if is_parent
output+= "</li>"
end
end
output += "#{prefix} </ul>"
output
end
end
end
Liquid::Template.register_tag("navigation", Jekyll::NavTree)
It is ugly code, but it's a start. You can see it in action on https://sendgrid.com/docs