Convert a structured string to a tree - ruby-on-rails

I have strings formatted like this cookie,sandwich(hotdog,burger),cake(chocolate(tiramisu)),candy. I'd like to convert into a tree-like structure (can be hash/array):
cookie
sandwich
|__hotdog
|__burger
cake
|__chocolate
|__tiramisu
candy
What's the simplest way to do this? I looked at Treetop but it seems overkill.

str = "cookie,sandwich(hotdog,burger(cheese,onions)),cake(chocolate(tiramisu)),candy"
Let's first create a helper method to split strings on those commas that are separated by strings containing balanced parentheses.
def separate(str)
start_idx = 0
left_paren_count = 0
str.each_char.with_index.with_object([]) do |(c,i),a|
case c
when '('
left_paren_count += 1
when ')'
left_paren_count -= 1
when ','
if left_paren_count.zero?
a << str[start_idx..i-1]
start_idx = i+1
end
end
end << str[start_idx..-1]
end
For example,
separate(str)
#=> ["cookie",
# "sandwich(hotdog,burger(cheese,onions))",
# "cake(chocolate(tiramisu))",
# "candy"]
separate("hotdog,burger(cheese,onions)")
#=> ["hotdog",
# "burger(cheese,onions)"]
separate("cheese,onions")
#=> ["cheese", "onions"]
We may now write a recursive expression.
def recurse(str)
separate(str).map do |s,h|
s1, s2 = s.split('(', 2)
s.include?('(') ? [s1, recurse(s2[0..-2])] : s1
end
end
Try it.
recurse(str)
#=> ["cookie",
# ["sandwich", ["hotdog", ["burger", ["cheese", "onions"]]]],
# ["cake", [["chocolate", ["tiramisu"]]]], "candy"]

class Node
attr_reader :name
attr_reader :children
def initialize(str)
#name, children_str = str.match(/^(\w+)\((.+)\)$/)&.captures
nodes = (children_str || str)
.scan(/(\w+\([\w,]+\))|(\w+\([\w,()]+\))|(\w+)/)
.flatten.compact
if #name.nil? && nodes.size == 1
#name = nodes.first
else
#children = nodes.map { |s|
Node.new(s)
}
end
end
def show_tree(level=0)
str = ""
unless #name.nil?
str = " " * (level - 1) if level > 1
str += "|__" if level > 0
str += "#{#name}\n"
level += 1
end
#children&.each do |node|
str += "#{node.show_tree(level)}"
end
str
end
end
test
foods = Node.new("cookie,sandwich(hotdog,burger),cake(chocolate(tiramisu)),candy")
puts foods.show_tree
# cookie
# sandwich
# |__hotdog
# |__burger
# cake
# |__chocolate
# |__tiramisu
# candy
rails = Node.new("root(config,db(migrate,seeds),lib,app(controllers(concerns,api),models(concerns),views))")
puts rails.show_tree
# root
# |__config
# |__db
# |__migrate
# |__seeds
# |__lib
# |__app
# |__controllers
# |__concerns
# |__api
# |__models
# |__concerns
# |__views

Related

How to see if the characters of one string appear in order in a second string

Given two strings, "word" and "key", how can I write a method sequence_search(word, key) that returns true (else false) if the characters in "key" appear in the same order (but not necessarily contiguous) in "word"?
Suppose, for example, key = 'cat'. The method should return true if "word" equals "arcata", "c1a2t3" or "coat", but return false if "word" equals "cta".
Here is my attempt to answer the question:
def sequence_search(word, key)
arr = []
i = 0
while i < word.length
word[i].include?(key)
arr >> word[i]
end
i+= 1
end
if arr.join == key # line raising exception
return true
else false
end
end
When I run my code I get the exception:
NameError (undefined local variable or method `arr' for main:Object)
in the line indicated. Why? And is there a better way to write the method?
To determine the problem with your code it helpful to first format it properly.
def sequence_search(word, key)
arr = []
i = 0
while i < word.length
word[i].include?(key)
arr >> word[i]
end
i += 1
end
if arr.join == key # line raising exception
return true
else
false
end
end
As you see, the method does not end where you thought it did and there is an extra end. Some of the problems are as follows:
the while loop will never end because i is not incremented within the loop;
arr >> word[i] should be arr << word[i]
word[i].include?(key) has no effect, as it's return value is not used (you may want arr << word[i] if word[i].include?(key));
the logic is wrong: after correcting the code if word = "acat" (which contains "cat") you are trying to construct the array arr #=> ["a", "c", "a", "t"], which you will join (to produce "acat") and compare with "cat" (if arr.join == key), which would (erroneously) fail.
Here are two ways you could write the method.
Use String#index to step through word looking for each character of key
def sequence_search(word, key)
i = -1
key.each_char.all? { |c| i = word.index(c,i+1) }
end
sequence_search("arcata", "cat") #=> true
sequence_search("c1a2t3", "cat") #=> true
sequence_search("cta", "cat") #=> false
sequence_search("coat", "cat") #=> true
See String#index, with particular attention to the optional second argument, the offset into the string at which the search is to begin.
Use a regular expression
def sequence_search(word, key)
word.match?(/#{key.chars.join(".*")}/)
end
sequence_search("arcata", "cat") #=> true
sequence_search("c1a2t3", "cat") #=> true
sequence_search("cta", "cat") #=> false
sequence_search("coat", "cat") #=> true
When key = "cat",
/#{key.chars.join(".*")}/
#=> /c.*a.*t/
The regular expression reads, "match a 'c' followed by zero or more characters followed by 'a' followed by zero or more characters followed by 't'.
Delete every char which is not in the key, and check if the key is included in the remainder:
def sequence_search(str, key)
str.delete("^#{key}").include?(key) # ^ means "everything but"
end
I don't know what solution are you looking for, but this a quick solution for me:
def sequence_search(word, key)
arr = key.split('')
arr.each do |c|
return false if word.index(c) == nil
word.slice!(0, word.index(c) + 1)
return true if arr.last == c
end
end
sequence_search('cat', 'cat') #=> true
sequence_search('cdadsas', 'cat') #=> false
sequence_search('gdfgddfgcgddadsast', 'cat') #=> true
sequence_search('gdfgddfgcgddadsast', 'cat4') #=> false
Useful information:
require 'benchmark'
N = 100_000
puts 'Ruby %s' % RUBY_VERSION
def cary1(word, key)
i = -1
key.each_char.all? { |c| i = word.index(c,i+1) }
end
def cary2(word, key)
word.match?(/#{key.chars.join(".*")}/)
end
def steenslag(str, key)
str.delete("^#{key}").include?(key) # ^ means "everything but"
end
def estebes(word, key)
arr = key.split('')
arr.each do |c|
return false if word.index(c) == nil
word.slice!(0, word.index(c) + 1)
return true if arr.last == c
end
end
Benchmark.bmbm do |x|
x.report('cary1') { N.times { cary1("arcata", "cat") } }
x.report('cary2') { N.times { cary2("arcata", "cat") } }
x.report('steenslag') { N.times { steenslag("arcata", "cat") } }
x.report('estebes') { N.times { estebes("arcata", "cat") } }
end
# >> Ruby 2.7.1
# >> Rehearsal ---------------------------------------------
# >> cary1 0.128231 0.000218 0.128449 ( 0.128572)
# >> cary2 0.461305 0.000509 0.461814 ( 0.462048)
# >> steenslag 0.055794 0.000026 0.055820 ( 0.055847)
# >> estebes 0.263030 0.000185 0.263215 ( 0.263399)
# >> ------------------------------------ total: 0.909298sec
# >>
# >> user system total real
# >> cary1 0.131944 0.000141 0.132085 ( 0.132227)
# >> cary2 0.453452 0.000626 0.454078 ( 0.454374)
# >> steenslag 0.055342 0.000026 0.055368 ( 0.055394)
# >> estebes 0.255280 0.000156 0.255436 ( 0.255607)
Using Fruity:
require 'fruity'
puts 'Ruby %s' % RUBY_VERSION
def cary1(word, key)
i = -1
key.each_char.all? { |c| i = word.index(c,i+1) }
end
def cary2(word, key)
word.match?(/#{key.chars.join(".*")}/)
end
def steenslag(str, key)
str.delete("^#{key}").include?(key) # ^ means "everything but"
end
def estebes(word, key)
arr = key.split('')
arr.each do |c|
return false if word.index(c) == nil
word.slice!(0, word.index(c) + 1)
return true if arr.last == c
end
end
compare do
_cary1 { cary1("arcata", "cat") }
_cary2 { cary2("arcata", "cat") }
_steenslag { steenslag("arcata", "cat") }
_estebes { estebes("arcata", "cat") }
end
# >> Ruby 2.7.1
# >> Running each test 8192 times. Test will take about 2 seconds.
# >> _steenslag is faster than _cary1 by 2x ± 0.1
# >> _cary1 is faster than _estebes by 2x ± 0.1
# >> _estebes is faster than _cary2 by 2x ± 0.1

Finding letters that are near, exact or not in a user input string

I am currently developing a small modified version of Hangman in Rails for children. The game starts by randomly generating a word from a text file and the user has to guess the word by entering a four letter word. Each word is the split by each character for example "r", "e", "a", "l" and returns a message on how they are to the word.
Random Generated word is "real"
Input
rlax
Output
Correct, Close, Correct, Incorrect
I have tried other things which I have found online but haven't worked and I am fairly new to Ruby and Rails. Hopefully someone can guide me in the right direction.
Here is some code
def letterCheck(lookAtLetter)
lookAHead = lookAtLetter =~ /[[:alpha:]]/
end
def displayWord
$ranWordBool.each_index do |i|
if($ranWordBool[i])
print $ranWordArray[i]
$isWin += 1
else
print "_"
end
end
end
def gameLoop
turns = 10
turnsLeft = 0
lettersUsed = []
while(turnsLeft < turns)
$isWin = 0
displayWord
if($isWin == $ranWordBool.length)
system "cls"
puts "1: Quit"
puts "The word is #{$ranWord} and You Win"
puts "Press any key to continue"
return
end
print "\n" + "Words Used: "
lettersUsed.each_index do |looper|
print " #{lettersUsed[looper]} "
end
puts "\n" + "Turns left: #{turns - turnsLeft}"
puts "Enter a word"
input = gets.chomp
system "cls"
if(input.length != 4)
puts "Please enter 4 lettered word"
elsif(letterCheck(input))
if(lettersUsed.include?(input))
puts "#{input} already choosen"
elsif($ranWordArray.include?(input))
puts "Close"
$ranWordArray.each_index do |i|
if(input == $ranWordArray[i])
$ranWordBool[i] = true
end
if($ranWordBool[i] = true)
puts "Correct"
else
puts "Incorrect"
end
end
else
lettersUsed << input
turnsLeft += 1
end
else
puts "Not a letter"
end
end
puts "You lose"
puts "The word was #{$ranWord}"
puts "Press any key to continue"
end
words = []
File.foreach('words.txt') do |line|
words << line.chomp
end
while(true)
$ranWord = words[rand(words.length) + 1]
$ranWordArray = $ranWord.chars
$ranWordBool = []
$ranWordArray.each_index do |i|
$ranWordBool[i] = false
end
system "cls"
gameLoop
input = gets.chomp
shouldQuit(input)
end
Something like that:
# Picking random word to guess
word = ['open', 'real', 'hang', 'mice'].sample
loop do
puts "So, guess the word:"
input_word = gets.strip
if word == input_word
puts("You are right, the word is: #{input_word}")
break
end
puts "You typed: #{input_word}"
# Split both the word to guess and the suggested word into array of letters
word_in_letters = word.split('')
input_in_letters = input_word.split('')
result = []
# Iterate over each letter in the word to guess
word_in_letters.each_with_index do |letter, index|
# Pick the corresponding letter in the entered word
letter_from_input = input_in_letters[index]
if letter == letter_from_input
result << "#{letter_from_input} - Correct"
next
end
# Take nearby letters by nearby indexes
# `reject` is here to skip negative indexes
# ie: letter 'i' in a word "mice"
# this will return 'm' and 'c'
# ie: letter 'm' in a word "mice"
# this will return 'i'
letters_around =
[index - 1, index + 1]
.reject { |i| i < 0 }
.map { |i| word_in_letters[i] }
if letters_around.include?(letter_from_input)
result << "#{letter_from_input} - Close"
next
end
result << "#{letter_from_input} - Incorrect"
end
puts result.join("\n")
end

A ruby script and a rails model: Same code - different behaviour

I have coded the following ruby script:
require 'open-uri'
require 'Nokogiri'
require 'anemone'
class JobFox
attr_accessor :company_url,
:jobs_page,
:max_words,
:jobs_part,
:jobs_container,
:element_score,
:max_score,
:jobs
def calc_element_score(element)
self.element_score += (element['class'].to_s.scan(/job|career|position|opening/).count + element['id'].to_s.scan(/job|career|position|opening/).count) * 100
self.element_score += element.to_s.scan(/job|career|position|opening/).count * 5
element.css('a').each do |a|
self.element_score += a.to_s.scan(/job|career|position|opening/).count * 7
end
element.css('li').each do |li|
self.element_score += li.to_s.scan(/job|career|position|opening/).count * 5
end
element.css('h').each do |h|
self.element_score += h.to_s.scan(/job|career|position|opening/).count * 3
end
if self.element_score > self.max_score
self.max_score = self.element_score
self.jobs_part = element
end
if element.children.count == 0
self.element_score = 0
end
end
end
fox = JobFox.new
fox.company_url = 'http://www.website.com'
fox.max_words = 0
fox.jobs = []
# CRAWL THE WEBSITE TO FIND THE JOBS LINK
Anemone.crawl(fox.company_url, :depth_limit => 3) do |anemone|
anemone.on_pages_like(/job|jobs|career|careers|team|about/) do |page|
begin
puts "SCANNING: " + page.url.to_s
# SCAN THE HTML AND FIND THE OCCURENCES OF THE WORD "JOB"
source_html = open(page.url).read
job_occurences = source_html.scan(/job|jobs|work|position/).count
# IF MORE OCCURENCES THAN BEFORE, WE KEEP THE PAGE URL
if job_occurences > fox.max_words
fox.max_words = job_occurences
fox.jobs_page = page.url
end
rescue Exception => e
puts e
end
end
end
fox.jobs_container = Nokogiri::HTML(open(fox.jobs_page))
fox.element_score = fox.max_score = 0
fox.jobs_container.css('div, section').each do |container|
container.traverse do |element|
fox.calc_element_score(element)
end
end
fox.jobs_part.traverse do |element|
element.css('a').each do |job|
fox.jobs << job.text
end
end
# REMOVE POSSIBLE DUPLICATE ENTRIES
fox.jobs = fox.jobs.uniq
puts fox.jobs
and I am trying to port it to a rails application - not as a script/task but as a model function:
require 'anemone'
require 'open-uri'
require 'Nokogiri'
class Company < ActiveRecord::Base
has_many :jobs
accepts_nested_attributes_for :jobs
# CALCULATE THE RELATEDNESS OF EACH HTML ELEMENT
def calculate_element_score(element)
#jobs_expression = '/job|career|position|opening/'
#element_score += (element['class'].to_s.scan(#jobs_expression).count + element['id'].to_s.scan(#jobs_expression).count) * 100
#element_score += element.to_s.scan(#jobs_expression).count * 5
element.css('a').each do |a|
#element_score += a.to_s.scan(#jobs_expression).count * 7
end
element.css('li').each do |li|
#element_score += li.to_s.scan(#jobs_expression).count * 5
end
element.css('h').each do |h|
#element_score += h.to_s.scan(#jobs_expression).count * 3
end
if #element_score > #max_score
#max_score = #element_score
#jobs_part = element
end
if element.children.count == 0
#element_score = 0
end
end
# CRAWL THE WEBSITE TO FIND THE JOBS PAGE
def find_jobs_page
max_words = 0
Anemone.crawl(self.website, :depth_limit => 3) do |anemone|
anemone.on_pages_like(/job|jobs|career|careers|team|about/) do |page|
begin
# SCAN THE HTML AND FIND OCCURENCES OF RELEVANT WORDS
source_html = open(page.url).read
job_occurences = source_html.scan(/job|jobs|work|position/).count
# IF MORE OCCURENCES THAN BEFORE, KEEP THE PAGE URL
if job_occurences > max_words
max_words = job_occurences
self.jobs_page = page.url
end
rescue Exception => e
puts e
end
end
end
end
# FIND THE CONTAINER THAT HAS THE JOB LISTINGS
def find_jobs_container
jobs_container = Nokogiri::HTML(open(self.jobs_page))
#element_score = #max_score = 0
#jobs_expression = '/job|career|position|opening/'
jobs_container.css('div, section').each do |container|
container.traverse do |element|
self.calculate_element_score(element)
end
end
end
# ADD THE JOBS FROM THE PAGE TO THE COMPANY ASSOCIATION
def extract_jobs
#jobs_part.traverse do |element|
element.css('a').each do |job|
j = JOBS.new()
j.title = job.text
j.url = job
self.jobs << j
end
end
end
# THE METHOD TO FIND ALL THE JOBS FOR A COMPANY
def find_jobs
self.find_jobs_page
self.find_jobs_container
self.extract_jobs
end
end
Everything works just fine apart from the calculate_element_score method - #elements_score is always 0. Have I understood something entirely wrong regarding global variables?

Arel producing different SQL output

I'm writing a gem for Rails that makes use of Arel. I have run into a case where the output generated by a subnode is different depending on how the output is generated. Below is a test case. Is this a bug or am I doing something wrong?
it 'should generate the same output' do
def ts_language; 'english'; end
def searchable_columns; [:id, :name]; end
def ts_column_sets
column_sets = if searchable_columns[0].is_a?(Array)
searchable_columns.map do |array|
array.map { |c| Table.new(:users)[c] }
end
else
searchable_columns.map { |c| Table.new(:users)[c] }.map { |x| [x] }
end
end
def ts_vectors
ts_column_sets.map do |columns|
coalesce = columns[1..-1].inject(columns[0]) do |memo, column|
Arel::Nodes::InfixOperation.new('||', memo, column)
end
coalesce = Arel::Nodes::InfixOperation.new('::', Arel::Nodes::NamedFunction.new('COALESCE', [coalesce, '']), Arel::Nodes::SqlLiteral.new('text'))
Arel::Nodes::NamedFunction.new('to_tsvector', [ts_language, coalesce])
end
end
def ts_query(query)
querytext = query.is_a?(Array) ? query.map(&:to_s).map(&:strip) : query.to_s.strip.split(" ")
querytext = querytext[1..-1].inject(querytext[0]) { |memo, c| memo + ' & ' + c }
querytext << ':*'
querytext = Arel::Nodes::InfixOperation.new('::', querytext, Arel::Nodes::SqlLiteral.new('text'))
Arel::Nodes::NamedFunction.new('to_tsquery', ['english', querytext])
end
node = Arel::Nodes::InfixOperation.new('##', ts_vectors[0], ts_query(0))
assert_equal 'to_tsvector(\'english\', COALESCE("users"."id", 0) :: text)', node.left.to_sql
assert_equal 'to_tsquery(\'english\', \'0:*\' :: text)', node.right.to_sql
assert_equal 'to_tsvector(\'english\', COALESCE("users"."id", 0) :: text) ## to_tsquery(0, \'0:*\' :: text)', node.to_sql
end
The line
assert_equal 'to_tsquery(\'english\', \'0:*\' :: text)', node.right.to_sql
is correct but the line
assert_equal 'to_tsvector(\'english\', COALESCE("users"."id", 0) :: text) ## to_tsquery(0, \'0:*\' :: text)', node.to_sql
results in an error and the output is:
'to_tsvector(\'english\', COALESCE("users"."id", 0) :: text) ## to_tsquery(0, 0 :: text)'

Ruby way to Check for string palindrome

I wanted to check if a string is palindrome or not using ruby code.
I am a starter in ruby so not too aquainted with the string methods in ruby
If you are not acquainted with Ruby's String methods, you should have a look at the documentation, it's very good. Mithun's answer already showed you the basic principle, but since you are new to Ruby, there's a couple more things to keep in mind:
*) If you have a predicate method, it's customary to name it with a trailing question mark, e.g. palindrome?.
*) Boolean expressions evaluate to a boolean, so you don't need to explicitly return true or false. Hence a short idiomatic version would be
def palindrome?(str)
str == str.reverse
end
*) Since Ruby's classes are open, you could add this to the string class:
class String
def palindrome?
self == self.reverse
end
end
*) If you don't want to monkey-patch String, you can directly define the method on single object (or use a module and Object#extend):
foo = "racecar"
def foo.palindrome?
self == self.reverse
end
*) You might want to make the palindrome check a bit more complex, e.g. when it comes to case or whitespace, so you are also able to detect palindromic sentences, capitalized words like "Racecar" etc.
pal = "Never a foot too far, even."
class String
def palindrome?
letters = self.downcase.scan(/\w/)
letters == letters.reverse
end
end
pal.palindrome? #=> true
def check_palindromic(variable)
if variable.reverse == variable #Check if string same when reversed
puts "#{ variable } is a palindrome."
else # If string is not the same when reversed
puts "#{ variable } is not a palindrome."
end
end
The recursive solution shows how strings can be indexed in Ruby:
def palindrome?(string)
if string.length == 1 || string.length == 0
true
else
if string[0] == string[-1]
palindrome?(string[1..-2])
else
false
end
end
end
If reading the Ruby string documentation is too boring for you, try playing around with the Ruby practice questions on CodeQuizzes and you will pick up most of the important methods.
def is_palindrome(value)
value.downcase!
# Reverse the string
reversed = ""
count = value.length
while count > 0
count -= 1
reversed += value[count]
end
# Instead of writing codes for reverse string
# we can also use reverse ruby method
# something like this value == value.reverse
if value == reversed
return "#{value} is a palindrom"
else
return "#{value} is not a palindrom"
end
end
puts "Enter a Word"
a = gets.chomp
p is_palindrome(a)
class String
def palindrome?
self.downcase == self.reverse.downcase
end
end
puts "racecar".palindrome? # true
puts "Racecar".palindrome? # true
puts "mississippi".palindrome? # false
str= gets.chomp
str_rev=""
n=1
while str.length >=n
str_rev+=str[-n]
n+=1
end
if str_rev==str
puts "YES"
else
puts "NO"
end
> first method
a= "malayalam"
if a == a.reverse
puts "a is true"
else
puts "false"
end
> second one
a= "malayalam"
a=a.split("")
i=0
ans=[]
a.count.times do
i=i+1
k=a[-(i)]
ans << k
end
if a== ans
puts "true"
else
puts "false"
end
def palindrome?(string)
string[0] == string[-1] && (string.length <= 2 || palindrome?(string[1..-2]))
end
**Solution 1** Time complexity = O(n), Space complexity = O(n)
This solution does not use the reverse method of the String class. It uses a stack(we could use an array that only allows entry and exit of elements from one end to mimic a stack).
def is_palindrome(str)
stack = []
reversed_str = ''
str.each_char do |char|
stack << char
end
until stack.empty?
reversed_str += stack.pop
end
if reversed_str == str
return true
else
return false
end
end
` Solution 2: Time complexity = O(n), Space complexity = O(1)
def inplace_reversal!(str)
i =0
j = str.length - 1
while i < j
temp = str[i]
str[i] = str[j]
str[j] = temp
i+=1
j-=1
end
return str
end
def palindrome?(str)
return "Please pass the string" if str.nil?
str = str.downcase
str_array = str.split('')
reverse_string = str_array.each_index{ |index| str_array[str_array.count - index - 1 ] end
return ("String #{str} is not a palindrome") unless str == reverse_string.join('')
"String #{str} is palindrome"
end

Resources